mirror of
https://github.com/logseq/logseq.git
synced 2026-05-23 12:14:06 +00:00
6
deps/workers/README.md
vendored
6
deps/workers/README.md
vendored
@@ -42,12 +42,6 @@ Local split note:
|
||||
- `worker/wrangler.agents.toml` sets `AGENT_RUNTIME_PROVIDER=e2b` by default.
|
||||
- On localhost, `/sessions*` forwarding retries during agents startup (up to ~30s) to avoid transient `503`.
|
||||
|
||||
Planning note:
|
||||
- planning APIs live in the same `logseq-agents` service as execution APIs
|
||||
- current planning entrypoint is `POST /planning/sessions`
|
||||
- this keeps auth, managed ChatGPT token reuse, runtime dispatch, and future agent types inside one unified agent service
|
||||
- `/sessions*`, `/planning*`, and future agent endpoints should continue to share the same worker unless there is a clear operational reason to split them
|
||||
|
||||
Production routing note:
|
||||
- If `api.logseq.com` is currently routed via AWS Route53/API Gateway, keep hostname routing in API Gateway.
|
||||
- Forward only `/sessions*` from API Gateway to the deployed agents worker URL (`*.workers.dev` or another worker-facing domain).
|
||||
|
||||
@@ -24,5 +24,3 @@ Milestones are tracked as separate files in this folder:
|
||||
- `21-m21-store-snapshots-metadata-in-d1.md`
|
||||
- `23-m23-local-runner-via-tunnel.md`
|
||||
- `24-m24-e2b-sandbox-runtime-default.md`
|
||||
- `25-m25-chatgpt-login-token-auth.md`
|
||||
- `26-m26-cloudflare-native-planning-layer.md`
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
# M26: Cloudflare-Native Planning Layer
|
||||
|
||||
Status: Proposed
|
||||
Target: Add a planning control plane on top of the existing `/sessions`
|
||||
agents service by using Cloudflare Agents for interactive planning chat
|
||||
and state, and Cloudflare Workflows for durable planning,
|
||||
approval, execution dispatch, and replanning flows.
|
||||
|
||||
## Goal
|
||||
|
||||
Introduce a planning layer above the existing agents execution service
|
||||
while reducing home-made control-plane and planning-chat transport code
|
||||
as much as possible.
|
||||
|
||||
The planning layer should reuse the existing execution substrate rather
|
||||
than replace it.
|
||||
|
||||
The milestone also covers turning planner output into real Logseq
|
||||
`#Task` blocks and keeping those tasks synchronized with execution
|
||||
status as sessions run.
|
||||
|
||||
## Why M26
|
||||
|
||||
- The current agents service already provides the execution substrate for sandbox work and should be reused.
|
||||
- The missing system layer is durable planning and orchestration, not another execution backend.
|
||||
- Cloudflare Agents can reduce custom realtime planning chat and planning-session state code.
|
||||
- Cloudflare Workflows can reduce custom orchestration, retry, wait-state, and replanning logic.
|
||||
- Planning state and execution-operational state need clearer separation.
|
||||
|
||||
## Architectural Decision
|
||||
|
||||
- Keep the existing `/sessions` APIs and execution semantics.
|
||||
- Do not replace E2B, local-runner, runtime-provider, source-control, or checkpoint logic in this milestone.
|
||||
- Use Cloudflare Agent instances as the canonical planning-session model.
|
||||
- Use Workflows for direct planning, repo-aware planning, approval waits, execution fan-out, and replanning.
|
||||
- Keep the Logseq graph as the product-visible system of record for goals, plans, tasks, and execution summaries.
|
||||
- Accept that operational state remains split across Logseq graph, Cloudflare Agent/Workflow state, and the existing agents service.
|
||||
|
||||
## Scope
|
||||
|
||||
1. Define a planning-session model backed by Cloudflare Agents.
|
||||
2. Define a workflow-driven planning pipeline:
|
||||
- goal intake
|
||||
- clarification
|
||||
- direct planning
|
||||
- repo-aware research
|
||||
- approval gate
|
||||
- execution dispatch
|
||||
- replanning
|
||||
3. Define planner-created Logseq task persistence:
|
||||
- create real `#Task` blocks under the goal block by default
|
||||
- initialize new planner-created tasks as `Todo`
|
||||
- inherit `project` and `agent` from goal or plan context when available
|
||||
- use created task `:block/uuid` as the canonical task identity for later reconciliation
|
||||
4. Define the handoff from planned task to existing `sessions/create`.
|
||||
5. Define the frontend direction to replace custom planning-chat transport with Cloudflare-native client primitives where possible.
|
||||
6. Define persistence boundaries across Logseq graph, Agent/Workflow state, and existing execution-operational state.
|
||||
7. Define an initial rollout strategy that keeps the execution service unchanged.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Replacing `/sessions` execution APIs.
|
||||
- Replacing sandbox provisioning or runtime-provider logic.
|
||||
- Replacing checkpoint persistence.
|
||||
- Replacing managed auth and GitHub token logic.
|
||||
- Migrating all existing execution chat/session flows to Cloudflare Agents immediately.
|
||||
- Full frontend rewrite.
|
||||
|
||||
## Workstreams
|
||||
|
||||
### WS1: Planning Architecture and Contracts
|
||||
|
||||
- Define planning entities and lifecycle.
|
||||
- Define mapping from Goal, Plan, and Task into Logseq task blocks and execution-session payloads.
|
||||
- Define the planner task contract:
|
||||
- goal block parent
|
||||
- task title and content
|
||||
- `#Task` marker
|
||||
- initial `Todo` status
|
||||
- inherited `project`
|
||||
- inherited `agent`
|
||||
- optional dependency metadata
|
||||
- Define approval triggers, follow-up task creation, and replanning entrypoints.
|
||||
- Define reconciliation rules for replanning:
|
||||
- match by stored task `:block/uuid` when available
|
||||
- fall back to best-effort matching against existing non-started child tasks when necessary
|
||||
- update planning-owned fields before execution starts
|
||||
- avoid duplicate task creation
|
||||
- preserve execution-owned fields once a session exists
|
||||
|
||||
### WS2: Cloudflare Agent Planning Session
|
||||
|
||||
- Define a planning Agent instance as the canonical interactive planning session.
|
||||
- Use Cloudflare-native chat, state, and client primitives where possible.
|
||||
- Minimize or eliminate custom planning transport code.
|
||||
|
||||
### WS3: Workflow Orchestration
|
||||
|
||||
- Define Workflow steps for decomposition, repo-aware research, approval wait, execution dispatch, and replanning.
|
||||
- Define retry behavior, failure handling, and pause/resume semantics.
|
||||
- Define how Workflow state links back to Logseq graph records and planning sessions.
|
||||
|
||||
### WS4: Logseq Task Persistence
|
||||
|
||||
- Create planner-generated tasks as child blocks under the goal block.
|
||||
- Make planner-created tasks immediately runnable when inherited `project` and `agent` are present.
|
||||
- Reuse the existing runnable task shape already consumed by the agent session flow.
|
||||
- Preserve execution-owned fields such as session id, PR URL, checkpoint metadata, and terminal execution status after a task has started.
|
||||
|
||||
### WS5: Execution Handoff
|
||||
|
||||
- Define exactly how a planned task becomes a `POST /sessions` request.
|
||||
- Include project metadata, agent metadata, runtime-provider selection, optional runner pinning, checkpoint reuse, and capability flags.
|
||||
- Reuse the existing execution API instead of inventing a second runtime control surface.
|
||||
- Reuse the existing session-to-task-status mapping for planner-created tasks:
|
||||
- `created` / `running` -> `Doing`
|
||||
- `paused` -> `Todo`
|
||||
- `completed` -> `Done`
|
||||
- `pr-created` -> `In Review`
|
||||
- `failed` / `canceled` -> `Canceled`
|
||||
- Apply the same execution status sync to planner-created and manually created runnable tasks.
|
||||
|
||||
### WS6: Frontend Integration Strategy
|
||||
|
||||
- Reuse existing chat UI components where practical.
|
||||
- Replace custom planning-chat transport with Cloudflare Agent client APIs.
|
||||
- Keep execution chat/session UI on the existing service unless a later milestone migrates it.
|
||||
|
||||
### WS7: Validation and Rollout
|
||||
|
||||
- Validate that the planning layer can drive the existing execution backend without adding new custom orchestration endpoints.
|
||||
- Roll out behind a planning-specific feature flag or internal-only entrypoint.
|
||||
- Define degraded behavior if Cloudflare Agent or Workflow state is temporarily unavailable.
|
||||
|
||||
## Exit Criteria
|
||||
|
||||
1. A planning session can be represented as a Cloudflare Agent instance.
|
||||
2. A planning Workflow can orchestrate decomposition, approval, and execution dispatch durably.
|
||||
3. Planner-generated tasks appear in Logseq as real `#Task` blocks under the goal block.
|
||||
4. Planner-created tasks default to `Todo` and inherit `project` and `agent` when available.
|
||||
5. Replanning updates existing planner-created tasks without creating duplicates.
|
||||
6. Planned tasks have a documented translation into the existing `sessions/create` contract.
|
||||
7. The design clearly separates planning state from execution-operational state.
|
||||
8. The milestone reduces planned custom code by preferring Cloudflare-native planning primitives.
|
||||
9. Existing `/sessions` execution behavior remains unchanged.
|
||||
|
||||
## Validation
|
||||
|
||||
- Update the planning architecture document to reflect the new layering and persistence model.
|
||||
- Review the milestone against the current agents worker contracts before implementation starts.
|
||||
- Walk through the task-to-`sessions/create` mapping and confirm every required execution field is accounted for.
|
||||
- Validate planner-created task behavior:
|
||||
- tasks are created under the goal block
|
||||
- tasks are marked as `#Task`
|
||||
- tasks default to `Todo`
|
||||
- tasks inherit `project` and `agent` from goal or plan context when present
|
||||
- planner reruns reconcile by stored task `:block/uuid`, with title-based fallback before execution starts
|
||||
- execution updates status through the existing session-status mapping
|
||||
- replanning does not overwrite execution-owned fields after a task has started
|
||||
- Future implementation should add targeted tests for planning-to-execution contract handling and Workflow integration.
|
||||
|
||||
## Defaults Chosen
|
||||
|
||||
- Planner-created tasks live under the goal block by default.
|
||||
- Initial planner-created status is `Todo`.
|
||||
- `project` and `agent` are inherited from goal or plan context when present.
|
||||
- Planner-created and manual runnable tasks share the same execution status-sync path.
|
||||
- Created task `:block/uuid` is the canonical task identity after creation.
|
||||
1
deps/workers/package.json
vendored
1
deps/workers/package.json
vendored
@@ -27,7 +27,6 @@
|
||||
"dependencies": {
|
||||
"@sentry/cloudflare": "^10.38.0",
|
||||
"@sentry/node": "^10.38.0",
|
||||
"agents": "^0.7.5",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"e2b": "2.14.0",
|
||||
"shadow-cljs": "^3.3.4",
|
||||
|
||||
6
deps/workers/shadow-cljs.edn
vendored
6
deps/workers/shadow-cljs.edn
vendored
@@ -20,9 +20,7 @@
|
||||
:warnings {:fn-deprecated false
|
||||
:redef false}}
|
||||
:modules {:main {:exports {default logseq.agents.worker/worker
|
||||
AgentSessionDO logseq.agents.worker/AgentSessionDO
|
||||
PlanningSessionAgent logseq.agents.worker/PlanningSessionAgent
|
||||
PlanningWorkflow logseq.agents.worker/PlanningWorkflow}}}
|
||||
AgentSessionDO logseq.agents.worker/AgentSessionDO}}}
|
||||
:js-options {:js-provider :import}
|
||||
:closure-defines {shadow.cljs.devtools.client.env/enabled false
|
||||
goog.debug.LOGGING_ENABLED true}
|
||||
@@ -52,7 +50,7 @@
|
||||
:main logseq.agents.e2b-runtime-provider-test-runner/main}
|
||||
:agents-runtime-transport-test {:target :node-test
|
||||
:output-to "worker/dist/agents-runtime-transport-test.js"
|
||||
:ns-regexp "logseq\\.agents\\.(sandbox-test|runtime-provider-test|routes-test|request-test|planning-workflow-test|handler-test)$"
|
||||
:ns-regexp "logseq\\.agents\\.(sandbox-test|runtime-provider-test)$"
|
||||
:devtools {:enabled false}
|
||||
:main logseq.agents.runtime-transport-test-runner/main}
|
||||
:agents-m25-managed-auth-test {:target :node-test
|
||||
|
||||
1
deps/workers/src/logseq/agents/dispatch.cljs
vendored
1
deps/workers/src/logseq/agents/dispatch.cljs
vendored
@@ -17,7 +17,6 @@
|
||||
(http/json-response :worker/health {:ok true})
|
||||
|
||||
(or (string/starts-with? path "/auth")
|
||||
(string/starts-with? path "/planning")
|
||||
(string/starts-with? path "/sessions")
|
||||
(string/starts-with? path "/runners"))
|
||||
(agent-handler/handle-fetch #js {:env env} request)
|
||||
|
||||
201
deps/workers/src/logseq/agents/do.cljs
vendored
201
deps/workers/src/logseq/agents/do.cljs
vendored
@@ -2,7 +2,6 @@
|
||||
(:require [clojure.string :as string]
|
||||
[lambdaisland.glogi :as log]
|
||||
[logseq.agents.checkpoint-store :as checkpoint-store]
|
||||
[logseq.agents.planning-store :as planning-store]
|
||||
[logseq.agents.runner-store :as runner-store]
|
||||
[logseq.agents.runtime-provider :as runtime-provider]
|
||||
[logseq.agents.sandbox :as sandbox]
|
||||
@@ -95,7 +94,6 @@
|
||||
session))
|
||||
|
||||
(declare <append-event!)
|
||||
(declare non-empty-str session-requested-by send-runtime-message!)
|
||||
|
||||
(defn- session-task
|
||||
[session]
|
||||
@@ -275,176 +273,6 @@
|
||||
(defn- session-runtime-provider [session]
|
||||
(some-> (get-in session [:runtime :provider]) str string/lower-case))
|
||||
|
||||
(def ^:private session-status->planning-task-status
|
||||
{"created" "Doing"
|
||||
"running" "Doing"
|
||||
"paused" "Todo"
|
||||
"completed" "Done"
|
||||
"pr-created" "In Review"
|
||||
"failed" "Canceled"
|
||||
"canceled" "Canceled"})
|
||||
|
||||
(def ^:private terminal-planning-task-statuses
|
||||
#{"Done" "In Review" "Canceled"})
|
||||
|
||||
(defn- session-planning-session-id
|
||||
[session]
|
||||
(some-> (get-in (session-task session) [:source :node-id]) non-empty-str))
|
||||
|
||||
(defn- session-planning-task-uuid
|
||||
[session]
|
||||
(some-> (get-in (session-task session) [:task-uuid]) non-empty-str))
|
||||
|
||||
(defn- task-message-content
|
||||
[task]
|
||||
(or (some-> (get-in task [:intent :content]) non-empty-str)
|
||||
(some-> (:content task) non-empty-str)
|
||||
(some-> (:description task) non-empty-str)
|
||||
(some-> (:title task) non-empty-str)))
|
||||
|
||||
(defn- planning-task-terminal?
|
||||
[task]
|
||||
(contains? terminal-planning-task-statuses
|
||||
(some-> (:status task) non-empty-str)))
|
||||
|
||||
(defn- planning-task-active?
|
||||
[task]
|
||||
(or (string? (some-> (:session-id task) non-empty-str))
|
||||
(contains? #{"Doing" "Done" "In Review" "Canceled"}
|
||||
(some-> (:status task) non-empty-str))))
|
||||
|
||||
(defn- planning-dependencies-satisfied?
|
||||
[task task-by-uuid]
|
||||
(let [deps (if (sequential? (:dependencies task))
|
||||
(keep non-empty-str (:dependencies task))
|
||||
[])]
|
||||
(every? (fn [task-uuid]
|
||||
(planning-task-terminal? (get task-by-uuid task-uuid)))
|
||||
deps)))
|
||||
|
||||
(defn- next-sequenced-task
|
||||
[tasks current-task-uuid]
|
||||
(let [tasks (vec (or tasks []))
|
||||
task-by-uuid (into {}
|
||||
(keep (fn [task]
|
||||
(when-let [task-uuid (some-> (:task-uuid task) non-empty-str)]
|
||||
[task-uuid task])))
|
||||
tasks)
|
||||
current-index (or (some (fn [[idx task]]
|
||||
(when (= current-task-uuid (some-> (:task-uuid task) non-empty-str))
|
||||
idx))
|
||||
(map-indexed vector tasks))
|
||||
-1)]
|
||||
(some (fn [task]
|
||||
(when (and (not (planning-task-active? task))
|
||||
(planning-dependencies-satisfied? task task-by-uuid))
|
||||
task))
|
||||
(drop (inc current-index) tasks))))
|
||||
|
||||
(defn- update-planning-tasks-for-terminal-event
|
||||
[tasks current-task-uuid session-id session-status]
|
||||
(mapv (fn [task]
|
||||
(if (= current-task-uuid (some-> (:task-uuid task) non-empty-str))
|
||||
(cond-> (assoc task
|
||||
:session-id session-id
|
||||
:status (or (get session-status->planning-task-status session-status)
|
||||
(:status task)))
|
||||
(string? (task-message-content task)) (assoc :content (task-message-content task)))
|
||||
task))
|
||||
(or tasks [])))
|
||||
|
||||
(defn- mark-next-task-doing
|
||||
[tasks next-task-uuid session-id]
|
||||
(mapv (fn [task]
|
||||
(if (= next-task-uuid (some-> (:task-uuid task) non-empty-str))
|
||||
(assoc task :session-id session-id :status "Doing")
|
||||
task))
|
||||
(or tasks [])))
|
||||
|
||||
(defn- append-dispatch-session-record
|
||||
[dispatch-sessions session-id task planning-session]
|
||||
(let [task-uuid (some-> (:task-uuid task) non-empty-str)]
|
||||
(if-not (and (string? session-id) (string? task-uuid))
|
||||
(vec (or dispatch-sessions []))
|
||||
(let [existing (some #(when (= task-uuid (some-> (:task-uuid %) non-empty-str)) %) dispatch-sessions)
|
||||
record (or existing
|
||||
{:id session-id
|
||||
:task-uuid task-uuid
|
||||
:source {:node-id (:planning-session-id planning-session)
|
||||
:node-title (or (some-> planning-session :goal :title non-empty-str)
|
||||
(some-> planning-session :goal :node-title non-empty-str)
|
||||
"Planning Task")}
|
||||
:intent {:content (task-message-content task)}
|
||||
:project (:project planning-session)
|
||||
:agent (:agent planning-session)})]
|
||||
(conj (vec (remove #(= task-uuid (some-> (:task-uuid %) non-empty-str))
|
||||
(or dispatch-sessions [])))
|
||||
record)))))
|
||||
|
||||
(defn- <continue-planning-session!
|
||||
[^js self current-session next-task]
|
||||
(let [message (task-message-content next-task)
|
||||
user-id (or (session-requested-by current-session) "system")]
|
||||
(if-not (string? message)
|
||||
(p/resolved nil)
|
||||
(p/let [_ (<append-event! self {:type "audit.log"
|
||||
:data {:event "user-message"
|
||||
:kind "user"
|
||||
:by user-id
|
||||
:message message}
|
||||
:ts (common/now-ms)})
|
||||
latest-session (<get-session self)
|
||||
resumed-session (<maybe-resume-session-for-message! self latest-session user-id)
|
||||
runtime (:runtime resumed-session)
|
||||
provider (when (map? runtime)
|
||||
(runtime-provider/resolve-provider (.-env self) runtime))
|
||||
_ (send-runtime-message! self
|
||||
resumed-session
|
||||
runtime
|
||||
provider
|
||||
message
|
||||
"user")]
|
||||
true))))
|
||||
|
||||
(defn- <advance-planning-session-after-terminal-event!
|
||||
[^js self session-id session-status]
|
||||
(p/let [current-session (<get-session self)
|
||||
planning-session-id (some-> current-session session-planning-session-id)
|
||||
current-task-uuid (some-> current-session session-planning-task-uuid)]
|
||||
(when (and (map? current-session)
|
||||
(= session-id (:id current-session))
|
||||
(string? planning-session-id)
|
||||
(string? current-task-uuid)
|
||||
(contains? #{"completed" "failed" "canceled"} session-status))
|
||||
(p/let [planning-session (planning-store/<get-planning-session-by-id! (.-env self)
|
||||
planning-session-id)]
|
||||
(when (map? planning-session)
|
||||
(let [tasks (update-planning-tasks-for-terminal-event (get-in planning-session [:plan :tasks])
|
||||
current-task-uuid
|
||||
session-id
|
||||
session-status)
|
||||
next-task (when (and (= "completed" session-status)
|
||||
(true? (:auto-dispatch planning-session)))
|
||||
(next-sequenced-task tasks current-task-uuid))
|
||||
next-task-uuid (some-> next-task :task-uuid non-empty-str)
|
||||
tasks (if (string? next-task-uuid)
|
||||
(mark-next-task-doing tasks next-task-uuid session-id)
|
||||
tasks)
|
||||
updated-session (assoc planning-session
|
||||
:status (if (string? next-task-uuid) "dispatching" (:status planning-session))
|
||||
:plan (assoc (:plan planning-session) :tasks tasks)
|
||||
:dispatch-sessions (if (map? next-task)
|
||||
(append-dispatch-session-record (:dispatch-sessions planning-session)
|
||||
session-id
|
||||
next-task
|
||||
planning-session)
|
||||
(:dispatch-sessions planning-session)))]
|
||||
(p/let [_ (planning-store/<upsert-planning-session! (.-env self) updated-session)]
|
||||
(when (map? next-task)
|
||||
(<continue-planning-session! self
|
||||
current-session
|
||||
next-task)))))))))
|
||||
|
||||
(defn- sandbox-bound-runtime?
|
||||
[runtime]
|
||||
(and (map? runtime)
|
||||
@@ -814,15 +642,7 @@
|
||||
(when (= "session.completed" event-type)
|
||||
(<checkpoint-and-terminate-completed-runtime! self session-id))
|
||||
(when (= "session.canceled" event-type)
|
||||
(<terminate-runtime-on-status! self session-id "canceled"))
|
||||
(when (contains? #{"session.completed" "session.failed" "session.canceled"} event-type)
|
||||
(<advance-planning-session-after-terminal-event! self
|
||||
session-id
|
||||
(case event-type
|
||||
"session.completed" "completed"
|
||||
"session.failed" "failed"
|
||||
"session.canceled" "canceled"
|
||||
nil))))))))
|
||||
(<terminate-runtime-on-status! self session-id "canceled")))))))
|
||||
|
||||
(defn- <consume-events-stream! [^js self session-id runtime on-ready]
|
||||
(let [provider (runtime-provider/resolve-provider (.-env self) runtime)]
|
||||
@@ -1668,19 +1488,12 @@
|
||||
;; IMPORTANT: don't block returning the Response; write the initial backlog async
|
||||
(js/queueMicrotask
|
||||
(fn []
|
||||
(-> (p/let [events (<get-events self)
|
||||
events (session/filter-events events {:since-ts since-ts})]
|
||||
(doseq [event events]
|
||||
(when-not @closed?
|
||||
;; writer.write returns a promise; wait so order is preserved
|
||||
(p/let [_ (->promise (.write writer (sse-bytes event)))]
|
||||
nil))))
|
||||
(p/catch (fn [error]
|
||||
(when-not @closed?
|
||||
(log/error :agent/session-stream-backlog-failed
|
||||
{:error error
|
||||
:since-ts since-ts})
|
||||
(cleanup)))))))
|
||||
(p/let [events (<get-events self)
|
||||
events (session/filter-events events {:since-ts since-ts})]
|
||||
(doseq [event events]
|
||||
;; writer.write returns a promise; wait so order is preserved
|
||||
(p/let [_ (->promise (.write writer (sse-bytes event)))]
|
||||
nil)))))
|
||||
|
||||
(js/Response.
|
||||
(.-readable stream)
|
||||
|
||||
602
deps/workers/src/logseq/agents/handler.cljs
vendored
602
deps/workers/src/logseq/agents/handler.cljs
vendored
@@ -2,8 +2,6 @@
|
||||
(:require [clojure.string :as string]
|
||||
[lambdaisland.glogi :as log]
|
||||
[logseq.agents.managed-auth :as managed-auth]
|
||||
[logseq.agents.planning-store :as planning-store]
|
||||
[logseq.agents.planning-workflow :as planning-workflow]
|
||||
[logseq.agents.request :as agent-request]
|
||||
[logseq.agents.routes :as routes]
|
||||
[logseq.agents.runner-store :as runner-store]
|
||||
@@ -45,12 +43,6 @@
|
||||
[claims]
|
||||
(aget claims "sub"))
|
||||
|
||||
(defn- planning-session-id-from-route
|
||||
[route]
|
||||
(some-> (get-in route [:path-params :planning-session-id]) str string/trim not-empty))
|
||||
|
||||
(declare planning-agent-binding planning-workflow-binding)
|
||||
|
||||
(defn- codex-agent?
|
||||
[agent]
|
||||
(cond
|
||||
@@ -128,8 +120,7 @@
|
||||
:headers headers})]
|
||||
(.fetch stub forwarded-request))))
|
||||
|
||||
(defn- handle-session-create
|
||||
[{:keys [env request url claims]} normalize-request]
|
||||
(defn- handle-create [{:keys [env request url claims]}]
|
||||
(p/let [result (common/read-json request)]
|
||||
(if (nil? result)
|
||||
(http/bad-request "missing body")
|
||||
@@ -157,593 +148,12 @@
|
||||
(if-let [^js stub (session-stub env session-id)]
|
||||
(let [headers (base-headers request claims)
|
||||
_ (.set headers "x-stream-base" (.-origin url))
|
||||
task (normalize-request body')
|
||||
task (agent-request/normalize-session-create body')
|
||||
body-json (js/JSON.stringify (clj->js task))
|
||||
do-url (str (.-origin url) "/__session__/init")]
|
||||
(forward-request stub do-url "POST" headers body-json))
|
||||
(http/error-response "server error" 500)))))))))
|
||||
|
||||
(defn- handle-create [{:keys [env request url claims]}]
|
||||
(handle-session-create {:env env
|
||||
:request request
|
||||
:url url
|
||||
:claims claims}
|
||||
agent-request/normalize-session-create))
|
||||
|
||||
(defn- normalize-planning-session-create
|
||||
[body claims]
|
||||
(let [planning-session-id (or (some-> (:planning-session-id body) str string/trim not-empty)
|
||||
(some-> (:session-id body) str string/trim not-empty)
|
||||
(str (random-uuid)))
|
||||
goal (if (map? (:goal body))
|
||||
(:goal body)
|
||||
(cond-> {}
|
||||
(some-> (:node-id body) str string/trim not-empty) (assoc :node-id (some-> (:node-id body) str string/trim))
|
||||
(some-> (:node-title body) str string/trim not-empty) (assoc :title (some-> (:node-title body) str string/trim))
|
||||
(some-> (:content body) str string/trim not-empty) (assoc :description (some-> (:content body) str string/trim))))
|
||||
require-approval (true? (:require-approval body))
|
||||
approval-status (if require-approval "pending" "approved")]
|
||||
{:planning-session-id planning-session-id
|
||||
:user-id (claims-user-id claims)
|
||||
:status "active"
|
||||
:goal goal
|
||||
:project (when (map? (:project body)) (:project body))
|
||||
:agent (:agent body)
|
||||
:approval-status approval-status
|
||||
:require-approval require-approval
|
||||
:auto-dispatch (if (boolean? (:auto-dispatch body))
|
||||
(:auto-dispatch body)
|
||||
false)
|
||||
:auto-replan (true? (:auto-replan body))
|
||||
:replan-delay-sec (if (number? (:replan-delay-sec body))
|
||||
(max 0 (:replan-delay-sec body))
|
||||
0)}))
|
||||
|
||||
(defn- handle-planning-create [{:keys [env request claims]}]
|
||||
(cond
|
||||
(not (planning-agent-binding env))
|
||||
(http/error-response "planning chat transport unavailable" 503)
|
||||
|
||||
(not (planning-store/available? env))
|
||||
(http/error-response "planning state unavailable" 503)
|
||||
|
||||
:else
|
||||
(p/let [result (common/read-json request)]
|
||||
(if (nil? result)
|
||||
(http/bad-request "missing body")
|
||||
(p/let [body' (js->clj result :keywordize-keys true)
|
||||
session (normalize-planning-session-create body' claims)
|
||||
planning-session-id (:planning-session-id session)
|
||||
user-id (:user-id session)
|
||||
params (agent-request/normalize-planning-workflow-create
|
||||
(merge body'
|
||||
{:planning-session-id planning-session-id
|
||||
:user-id user-id
|
||||
:goal (:goal session)
|
||||
:project (:project session)
|
||||
:agent (:agent session)
|
||||
:require-approval (:require-approval session)
|
||||
:auto-dispatch (:auto-dispatch session)
|
||||
:auto-replan (:auto-replan session)
|
||||
:replan-delay-sec (:replan-delay-sec session)}))
|
||||
params (when (map? params)
|
||||
(planning-workflow/enrich-params-with-model-plan env
|
||||
params
|
||||
{:timeout-ms 2500}))
|
||||
orchestrated (when (map? params)
|
||||
(planning-workflow/orchestrate-response params))]
|
||||
(if-not (and (string? planning-session-id)
|
||||
(string? user-id)
|
||||
(map? params)
|
||||
(map? orchestrated))
|
||||
(http/bad-request "invalid body")
|
||||
(-> (planning-store/<upsert-planning-session! env
|
||||
(assoc session
|
||||
:status (:status orchestrated)
|
||||
:goal (:goal params)
|
||||
:project (:project params)
|
||||
:agent (:agent params)
|
||||
:plan (assoc (:plan orchestrated)
|
||||
:tasks (:reconciled-tasks orchestrated))
|
||||
:scheduled-actions (:scheduled-actions orchestrated)
|
||||
:dispatch-sessions (:dispatch-sessions orchestrated)))
|
||||
(p/then (fn [_]
|
||||
(http/json-response :planning.sessions/create
|
||||
{:planning-session-id planning-session-id
|
||||
:status (:status orchestrated)
|
||||
:plan (assoc (:plan orchestrated)
|
||||
:tasks (:reconciled-tasks orchestrated))
|
||||
:scheduled-actions (:scheduled-actions orchestrated)
|
||||
:dispatch-sessions (:dispatch-sessions orchestrated)
|
||||
:approval-status (:approval-status session)
|
||||
:require-approval (true? (:require-approval session))
|
||||
:auto-dispatch (true? (:auto-dispatch session))
|
||||
:auto-replan (true? (:auto-replan session))
|
||||
:replan-delay-sec (:replan-delay-sec session)
|
||||
:chat-path (str "/planning/chat/" planning-session-id)})))
|
||||
(p/catch (fn [error]
|
||||
(log/error :agent/planning-session-create-failed error)
|
||||
(http/error-response "failed to create planning session" 500))))))))))
|
||||
|
||||
(defn- planning-workflow-binding [^js env]
|
||||
(aget env "PLANNING_WORKFLOW"))
|
||||
|
||||
(defn- planning-agent-binding [^js env]
|
||||
(aget env "PLANNING_AGENT"))
|
||||
|
||||
(defn- planning-agent-stub
|
||||
[^js env planning-session-id]
|
||||
(when-let [^js namespace (planning-agent-binding env)]
|
||||
(let [do-id (.idFromName namespace planning-session-id)]
|
||||
(.get namespace do-id))))
|
||||
|
||||
(def ^:private planning-agent-namespace "planning-agent")
|
||||
|
||||
(defn- planning-agent-request
|
||||
[request planning-session-id]
|
||||
(let [headers (js/Headers. (.-headers request))]
|
||||
(.set headers "x-partykit-room" planning-session-id)
|
||||
(.set headers "x-partykit-namespace" planning-agent-namespace)
|
||||
(js/Request. (.-url request)
|
||||
#js {:method (.-method request)
|
||||
:headers headers
|
||||
:body (when-not (= "GET" (.-method request))
|
||||
(.-body request))
|
||||
:duplex "half"})))
|
||||
|
||||
(defn- handle-planning-workflow-create [{:keys [env request claims]}]
|
||||
(cond
|
||||
(not (planning-workflow-binding env))
|
||||
(http/error-response "planning workflow unavailable" 503)
|
||||
|
||||
(not (planning-store/available? env))
|
||||
(http/error-response "planning state unavailable" 503)
|
||||
|
||||
:else
|
||||
(p/let [result (common/read-json request)]
|
||||
(if (nil? result)
|
||||
(http/bad-request "missing body")
|
||||
(p/let [user-id (claims-user-id claims)
|
||||
body' (js->clj result :keywordize-keys true)
|
||||
params (agent-request/normalize-planning-workflow-create body')
|
||||
planning-session-id (or (:planning-session-id params)
|
||||
(str (random-uuid)))
|
||||
params (cond-> params
|
||||
(string? planning-session-id) (assoc :planning-session-id planning-session-id)
|
||||
(string? user-id) (assoc :user-id user-id))
|
||||
params (when (map? params)
|
||||
(planning-workflow/enrich-params-with-model-plan env params))
|
||||
workflow-id (or (:workflow-id params)
|
||||
(str planning-session-id "-workflow"))
|
||||
workflow-binding (planning-workflow-binding env)]
|
||||
(if-not (and (map? params)
|
||||
(string? user-id)
|
||||
(string? planning-session-id))
|
||||
(http/bad-request "invalid body")
|
||||
(-> (.create workflow-binding
|
||||
(clj->js {:id workflow-id
|
||||
:params (dissoc params :workflow-id)}))
|
||||
(p/then (fn [instance]
|
||||
(let [workflow-id (or (some-> instance .-id)
|
||||
workflow-id)]
|
||||
(-> (planning-store/<upsert-planning-session! env
|
||||
{:planning-session-id planning-session-id
|
||||
:user-id user-id
|
||||
:workflow-id workflow-id
|
||||
:status (if (true? (:require-approval params))
|
||||
"waiting-approval"
|
||||
"queued")
|
||||
:goal (:goal params)
|
||||
:project (:project params)
|
||||
:agent (:agent params)
|
||||
:plan (assoc (:plan params)
|
||||
:tasks (:tasks params))
|
||||
:approval-status (if (true? (:require-approval params))
|
||||
"pending"
|
||||
"approved")
|
||||
:require-approval (true? (:require-approval params))
|
||||
:auto-dispatch (if (boolean? (:auto-dispatch params))
|
||||
(:auto-dispatch params)
|
||||
true)
|
||||
:auto-replan (true? (:auto-replan params))
|
||||
:replan-delay-sec (:replan-delay-sec params)})
|
||||
(p/then (fn [_]
|
||||
(http/json-response :planning.workflows/create
|
||||
{:planning-session-id planning-session-id
|
||||
:workflow-id workflow-id
|
||||
:status "queued"})))))))
|
||||
(p/catch (fn [error]
|
||||
(log/error :agent/planning-workflow-create-failed error)
|
||||
(http/error-response "failed to create planning workflow" 500))))))))))
|
||||
|
||||
(defn- handle-planning-workflow-get [{:keys [env route]}]
|
||||
(let [workflow-id (get-in route [:path-params :workflow-id])
|
||||
workflow-binding (planning-workflow-binding env)]
|
||||
(cond
|
||||
(not workflow-binding)
|
||||
(http/error-response "planning workflow unavailable" 503)
|
||||
|
||||
(not (string? workflow-id))
|
||||
(http/bad-request "invalid workflow id")
|
||||
|
||||
:else
|
||||
(-> (.get workflow-binding workflow-id)
|
||||
(p/then (fn [instance]
|
||||
(if-not instance
|
||||
(http/not-found)
|
||||
(-> (.status instance)
|
||||
(p/then (fn [status]
|
||||
(http/json-response :planning.workflows/get
|
||||
{:workflow-id workflow-id
|
||||
:status status})))))))
|
||||
(p/catch (fn [error]
|
||||
(log/error :agent/planning-workflow-get-failed error)
|
||||
(http/error-response "failed to fetch planning workflow" 500)))))))
|
||||
|
||||
(defn- <get-owned-planning-session!
|
||||
[^js env user-id planning-session-id]
|
||||
(if (and (planning-store/available? env)
|
||||
(string? user-id)
|
||||
(string? planning-session-id))
|
||||
(planning-store/<get-planning-session! env user-id planning-session-id)
|
||||
(p/resolved nil)))
|
||||
|
||||
(defn- <get-planning-agent-state!
|
||||
[^js env planning-session-id]
|
||||
(if-let [^js stub (planning-agent-stub env planning-session-id)]
|
||||
(let [request (platform/request "https://planning.internal/__planning__/state"
|
||||
#js {:method "GET"})]
|
||||
(-> (.fetch stub (planning-agent-request request planning-session-id))
|
||||
(p/then (fn [response]
|
||||
(if-not (.-ok response)
|
||||
nil
|
||||
(-> (.json response)
|
||||
(p/then (fn [payload]
|
||||
(let [state (js->clj payload :keywordize-keys true)]
|
||||
(when (map? state)
|
||||
state))))))))
|
||||
(p/catch (fn [_]
|
||||
nil))))
|
||||
(p/resolved nil)))
|
||||
|
||||
(defn- workflow-event-payload
|
||||
[event-type approval-status]
|
||||
(let [approved? (= "approved" approval-status)]
|
||||
{:type event-type
|
||||
:payload {:approved approved?
|
||||
:decision approval-status}}))
|
||||
|
||||
(defn- <send-workflow-event!
|
||||
[workflow-binding workflow-id event]
|
||||
(if-not (string? workflow-id)
|
||||
(p/rejected (ex-info "invalid workflow id" {:workflow-id workflow-id}))
|
||||
(-> (.get workflow-binding workflow-id)
|
||||
(p/then (fn [instance]
|
||||
(if-not instance
|
||||
(p/rejected (ex-info "workflow not found" {:workflow-id workflow-id}))
|
||||
(let [send-event (aget instance "sendEvent")]
|
||||
(if (fn? send-event)
|
||||
(.call send-event instance (clj->js event))
|
||||
(p/rejected (ex-info "workflow event transport unavailable"
|
||||
{:workflow-id workflow-id}))))))))))
|
||||
|
||||
(defn- planning-session-status-for-approval
|
||||
[approval-status]
|
||||
(case approval-status
|
||||
"approved" "queued"
|
||||
"rejected" "rejected"
|
||||
"pending" "waiting-approval"
|
||||
"waiting-approval"))
|
||||
|
||||
(defn- normalize-task-binding
|
||||
[binding]
|
||||
(when (map? binding)
|
||||
(let [task-uuid (some-> (:task-uuid binding) str string/trim not-empty)
|
||||
block-uuid (some-> (:block-uuid binding) str string/trim not-empty)
|
||||
session-id (some-> (:session-id binding) str string/trim not-empty)]
|
||||
(when (and task-uuid block-uuid)
|
||||
(cond-> {:task-uuid task-uuid
|
||||
:block-uuid block-uuid}
|
||||
(string? session-id) (assoc :session-id session-id))))))
|
||||
|
||||
(defn- merge-task-bindings
|
||||
[tasks bindings]
|
||||
(let [binding-by-task-uuid (->> bindings
|
||||
(keep normalize-task-binding)
|
||||
(reduce (fn [acc binding]
|
||||
(assoc acc (:task-uuid binding) binding))
|
||||
{}))]
|
||||
(mapv (fn [task]
|
||||
(if-let [binding (get binding-by-task-uuid
|
||||
(some-> (:task-uuid task) str string/trim not-empty))]
|
||||
(cond-> (assoc task :block-uuid (:block-uuid binding))
|
||||
(string? (:session-id binding)) (assoc :session-id (:session-id binding)
|
||||
:status (or (:status task) "Doing")))
|
||||
task))
|
||||
(or tasks []))))
|
||||
|
||||
(defn- handle-planning-session-get [{:keys [env claims route]}]
|
||||
(let [user-id (claims-user-id claims)
|
||||
planning-session-id (planning-session-id-from-route route)]
|
||||
(cond
|
||||
(not (planning-store/available? env))
|
||||
(http/error-response "planning state unavailable" 503)
|
||||
|
||||
(not (string? user-id))
|
||||
(http/unauthorized)
|
||||
|
||||
(not (string? planning-session-id))
|
||||
(http/bad-request "invalid planning session id")
|
||||
|
||||
:else
|
||||
(p/let [planning-session (<get-owned-planning-session! env
|
||||
user-id
|
||||
planning-session-id)]
|
||||
(if-not (map? planning-session)
|
||||
(http/not-found)
|
||||
(http/json-response :planning.sessions/get
|
||||
planning-session))))))
|
||||
|
||||
(defn- approval-decision
|
||||
[body]
|
||||
(or (some-> (get-in body [:approval :decision]) str string/trim string/lower-case not-empty)
|
||||
(some-> (:decision body) str string/trim string/lower-case not-empty)))
|
||||
|
||||
(defn- handle-planning-session-approval [{:keys [env request claims route]}]
|
||||
(let [user-id (claims-user-id claims)
|
||||
planning-session-id (planning-session-id-from-route route)
|
||||
workflow-binding (planning-workflow-binding env)]
|
||||
(cond
|
||||
(not (planning-store/available? env))
|
||||
(http/error-response "planning state unavailable" 503)
|
||||
|
||||
(not (string? user-id))
|
||||
(http/unauthorized)
|
||||
|
||||
(not (string? planning-session-id))
|
||||
(http/bad-request "invalid planning session id")
|
||||
|
||||
:else
|
||||
(p/let [result (common/read-json request)
|
||||
body (if (nil? result) {} (js->clj result :keywordize-keys true))
|
||||
planning-state (<get-planning-agent-state! env planning-session-id)
|
||||
approval-status (approval-decision body)
|
||||
approval-status (if (contains? #{"pending" "approved" "rejected"} approval-status)
|
||||
approval-status
|
||||
"pending")
|
||||
existing (<get-owned-planning-session! env user-id planning-session-id)]
|
||||
(if-not (map? existing)
|
||||
(http/not-found)
|
||||
(let [workflow-id (some-> (:workflow-id existing) str string/trim not-empty)]
|
||||
(cond
|
||||
(and workflow-binding (string? workflow-id))
|
||||
(-> (<send-workflow-event! workflow-binding
|
||||
workflow-id
|
||||
(workflow-event-payload "approval" approval-status))
|
||||
(p/then (fn [_]
|
||||
(planning-store/<update-planning-session! env
|
||||
user-id
|
||||
planning-session-id
|
||||
{:approval-status approval-status
|
||||
:status (planning-session-status-for-approval approval-status)})))
|
||||
(p/then (fn [updated]
|
||||
(if-not (map? updated)
|
||||
(http/not-found)
|
||||
(http/json-response :planning.sessions/get updated))))
|
||||
(p/catch (fn [error]
|
||||
(log/error :agent/planning-session-approval-event-failed error)
|
||||
(http/error-response "failed to apply planning approval" 500))))
|
||||
|
||||
(and (= "approved" approval-status)
|
||||
workflow-binding)
|
||||
(p/let [params (planning-workflow/enrich-params-with-model-plan
|
||||
env
|
||||
{:planning-session-id planning-session-id
|
||||
:user-id user-id
|
||||
:goal (:goal existing)
|
||||
:project (:project existing)
|
||||
:agent (:agent existing)
|
||||
:tasks (get-in existing [:plan :tasks])
|
||||
:planning-messages (:messages planning-state)
|
||||
:require-approval (true? (:require-approval existing))
|
||||
:approval {:decision "approved"
|
||||
:comment (some-> (get-in body [:approval :comment])
|
||||
str
|
||||
string/trim
|
||||
not-empty)}
|
||||
:auto-dispatch (if (boolean? (:auto-dispatch existing))
|
||||
(:auto-dispatch existing)
|
||||
true)
|
||||
:auto-replan (true? (:auto-replan existing))
|
||||
:replan-delay-sec (:replan-delay-sec existing)})
|
||||
workflow-id (or workflow-id
|
||||
(str planning-session-id "-workflow"))]
|
||||
(-> (.create workflow-binding
|
||||
(clj->js {:id workflow-id
|
||||
:params params}))
|
||||
(p/then (fn [instance]
|
||||
(let [workflow-id (or (some-> instance .-id)
|
||||
workflow-id)]
|
||||
(planning-store/<update-planning-session! env
|
||||
user-id
|
||||
planning-session-id
|
||||
{:approval-status "approved"
|
||||
:workflow-id workflow-id
|
||||
:status "queued"}))))
|
||||
(p/then (fn [updated]
|
||||
(if-not (map? updated)
|
||||
(http/not-found)
|
||||
(http/json-response :planning.sessions/get updated))))
|
||||
(p/catch (fn [error]
|
||||
(log/error :agent/planning-session-approval-failed error)
|
||||
(http/error-response "failed to queue approved planning workflow" 500)))))
|
||||
|
||||
:else
|
||||
(p/let [updated (planning-store/<update-planning-session! env
|
||||
user-id
|
||||
planning-session-id
|
||||
{:approval-status approval-status
|
||||
:status (planning-session-status-for-approval approval-status)})]
|
||||
(if-not (map? updated)
|
||||
(http/not-found)
|
||||
(http/json-response :planning.sessions/get updated))))))))))
|
||||
|
||||
(defn- handle-planning-session-task-sync [{:keys [env request claims route]}]
|
||||
(let [user-id (claims-user-id claims)
|
||||
planning-session-id (planning-session-id-from-route route)]
|
||||
(cond
|
||||
(not (planning-store/available? env))
|
||||
(http/error-response "planning state unavailable" 503)
|
||||
|
||||
(not (string? user-id))
|
||||
(http/unauthorized)
|
||||
|
||||
(not (string? planning-session-id))
|
||||
(http/bad-request "invalid planning session id")
|
||||
|
||||
:else
|
||||
(p/let [existing (<get-owned-planning-session! env user-id planning-session-id)
|
||||
result (common/read-json request)]
|
||||
(if-not (map? existing)
|
||||
(http/not-found)
|
||||
(let [body (if (nil? result) {} (js->clj result :keywordize-keys true))
|
||||
bindings (if (sequential? (:tasks body))
|
||||
(vec (:tasks body))
|
||||
[])
|
||||
merged-tasks (merge-task-bindings (get-in existing [:plan :tasks]) bindings)
|
||||
updated-session (assoc existing :plan (assoc (:plan existing) :tasks merged-tasks))]
|
||||
(p/let [updated (planning-store/<upsert-planning-session! env updated-session)]
|
||||
(http/json-response :planning.sessions/get
|
||||
(or updated
|
||||
updated-session)))))))))
|
||||
|
||||
(defn- handle-planning-session-replan [{:keys [env request claims route]}]
|
||||
(let [workflow-binding (planning-workflow-binding env)
|
||||
user-id (claims-user-id claims)
|
||||
planning-session-id (planning-session-id-from-route route)]
|
||||
(cond
|
||||
(not workflow-binding)
|
||||
(http/error-response "planning workflow unavailable" 503)
|
||||
|
||||
(not (planning-store/available? env))
|
||||
(http/error-response "planning state unavailable" 503)
|
||||
|
||||
(not (string? user-id))
|
||||
(http/unauthorized)
|
||||
|
||||
(not (string? planning-session-id))
|
||||
(http/bad-request "invalid planning session id")
|
||||
|
||||
:else
|
||||
(p/let [existing (<get-owned-planning-session! env user-id planning-session-id)
|
||||
result (common/read-json request)
|
||||
planning-state (<get-planning-agent-state! env planning-session-id)]
|
||||
(if-not (map? existing)
|
||||
(http/not-found)
|
||||
(let [body (if (nil? result)
|
||||
{}
|
||||
(js->clj result :keywordize-keys true))
|
||||
params (agent-request/normalize-planning-workflow-create
|
||||
(merge {:planning-session-id planning-session-id
|
||||
:user-id user-id
|
||||
:goal (:goal existing)
|
||||
:project (:project existing)
|
||||
:agent (:agent existing)
|
||||
:planning-messages (:messages planning-state)
|
||||
:require-approval (:require-approval existing)
|
||||
:auto-dispatch (:auto-dispatch existing)
|
||||
:auto-replan (:auto-replan existing)
|
||||
:replan-delay-sec (:replan-delay-sec existing)}
|
||||
body))
|
||||
params (when (map? params)
|
||||
(planning-workflow/enrich-params-with-model-plan env params))
|
||||
existing-workflow-id (some-> (:workflow-id existing) str string/trim not-empty)
|
||||
workflow-id (or (:workflow-id params)
|
||||
existing-workflow-id
|
||||
(str planning-session-id "-replan-" (random-uuid)))
|
||||
plan-tasks (or (:tasks params)
|
||||
(get-in existing [:plan :tasks]))
|
||||
update-data {:workflow-id workflow-id
|
||||
:status (if (true? (:require-approval params))
|
||||
"waiting-approval"
|
||||
"queued")
|
||||
:plan {:tasks plan-tasks}
|
||||
:approval-status (if (true? (:require-approval params))
|
||||
"pending"
|
||||
"approved")
|
||||
:require-approval (true? (:require-approval params))
|
||||
:auto-dispatch (if (boolean? (:auto-dispatch params))
|
||||
(:auto-dispatch params)
|
||||
true)
|
||||
:auto-replan (true? (:auto-replan params))
|
||||
:replan-delay-sec (:replan-delay-sec params)}]
|
||||
(if (string? existing-workflow-id)
|
||||
(-> (<send-workflow-event! workflow-binding
|
||||
existing-workflow-id
|
||||
{:type "replan"
|
||||
:payload {:params (assoc (dissoc params :workflow-id)
|
||||
:workflow-id existing-workflow-id)}})
|
||||
(p/then (fn [_]
|
||||
(planning-store/<update-planning-session! env
|
||||
user-id
|
||||
planning-session-id
|
||||
(assoc update-data :workflow-id existing-workflow-id))))
|
||||
(p/then (fn [updated]
|
||||
(http/json-response :planning.sessions/get
|
||||
(or updated
|
||||
existing))))
|
||||
(p/catch (fn [error]
|
||||
(log/error :agent/planning-session-replan-event-failed error)
|
||||
(http/error-response "failed to queue replanning workflow" 500))))
|
||||
(-> (.create workflow-binding
|
||||
(clj->js {:id workflow-id
|
||||
:params (assoc (dissoc params :workflow-id)
|
||||
:workflow-id workflow-id)}))
|
||||
(p/then (fn [instance]
|
||||
(let [workflow-id (or (some-> instance .-id)
|
||||
workflow-id)]
|
||||
(planning-store/<update-planning-session! env
|
||||
user-id
|
||||
planning-session-id
|
||||
(assoc update-data :workflow-id workflow-id)))))
|
||||
(p/then (fn [updated]
|
||||
(http/json-response :planning.sessions/get
|
||||
(or updated
|
||||
existing))))
|
||||
(p/catch (fn [error]
|
||||
(log/error :agent/planning-session-replan-failed error)
|
||||
(http/error-response "failed to queue replanning workflow" 500)))))))))))
|
||||
|
||||
(defn- handle-planning-chat-transport [{:keys [env request claims route]}]
|
||||
(let [planning-agent (planning-agent-binding env)
|
||||
planning-session-id (planning-session-id-from-route route)
|
||||
user-id (claims-user-id claims)]
|
||||
(cond
|
||||
(not planning-agent)
|
||||
(http/error-response "planning chat transport unavailable" 503)
|
||||
|
||||
(not (planning-store/available? env))
|
||||
(http/error-response "planning state unavailable" 503)
|
||||
|
||||
(not (string? user-id))
|
||||
(http/unauthorized)
|
||||
|
||||
(not (string? planning-session-id))
|
||||
(http/bad-request "invalid planning session id")
|
||||
|
||||
:else
|
||||
(p/let [owned-session (<get-owned-planning-session! env
|
||||
user-id
|
||||
planning-session-id)]
|
||||
(if-not (map? owned-session)
|
||||
(http/not-found)
|
||||
(if-let [^js stub (planning-agent-stub env planning-session-id)]
|
||||
(-> (.fetch stub (planning-agent-request request planning-session-id))
|
||||
(p/catch (fn [error]
|
||||
(log/error :agent/planning-chat-forward-failed error)
|
||||
(http/error-response "planning chat transport failed" 500))))
|
||||
(http/error-response "planning chat transport unavailable" 503)))))))
|
||||
|
||||
(defn- handle-auth-status [{:keys [env claims]}]
|
||||
(let [user-id (claims-user-id claims)]
|
||||
(if-not (string? user-id)
|
||||
@@ -975,14 +385,6 @@
|
||||
(case (:handler route)
|
||||
:auth.chatgpt/import (handle-auth-import ctx)
|
||||
:auth.chatgpt/status (handle-auth-status ctx)
|
||||
:planning.sessions/create (handle-planning-create ctx)
|
||||
:planning.sessions/get (handle-planning-session-get ctx)
|
||||
:planning.sessions/approval (handle-planning-session-approval ctx)
|
||||
:planning.sessions/tasks.sync (handle-planning-session-task-sync ctx)
|
||||
:planning.sessions/replan (handle-planning-session-replan ctx)
|
||||
:planning.chat/transport (handle-planning-chat-transport ctx)
|
||||
:planning.workflows/create (handle-planning-workflow-create ctx)
|
||||
:planning.workflows/get (handle-planning-workflow-get ctx)
|
||||
:sessions/create (handle-create ctx)
|
||||
:sessions/get (handle-get ctx)
|
||||
:sessions/messages (handle-messages ctx)
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
(ns logseq.agents.planning-agent
|
||||
(:require [clojure.string :as string]
|
||||
[logseq.sync.common :as common]
|
||||
[logseq.sync.platform.core :as platform]))
|
||||
|
||||
(defn- non-empty-str
|
||||
[value]
|
||||
(when (string? value)
|
||||
(let [trimmed (string/trim value)]
|
||||
(when-not (string/blank? trimmed)
|
||||
trimmed))))
|
||||
|
||||
(defn default-state
|
||||
[planning-session-id]
|
||||
{:planning-session-id planning-session-id
|
||||
:messages []
|
||||
:updated-at (common/now-ms)})
|
||||
|
||||
(defn- ensure-state-shape
|
||||
[planning-session-id state]
|
||||
(let [messages (if (sequential? (:messages state))
|
||||
(vec (:messages state))
|
||||
[])]
|
||||
(cond-> {:planning-session-id planning-session-id
|
||||
:messages messages
|
||||
:updated-at (common/now-ms)}
|
||||
(map? state) (merge (dissoc state :messages :updated-at :planning-session-id)))))
|
||||
|
||||
(defn append-message
|
||||
[planning-session-id state role content]
|
||||
(let [content (non-empty-str content)
|
||||
state (ensure-state-shape planning-session-id state)]
|
||||
(if-not (string? content)
|
||||
state
|
||||
(update (assoc state :updated-at (common/now-ms))
|
||||
:messages
|
||||
(fnil conj [])
|
||||
{:id (str (random-uuid))
|
||||
:role role
|
||||
:content content
|
||||
:ts (common/now-ms)}))))
|
||||
|
||||
(defn normalize-state
|
||||
[planning-session-id state]
|
||||
(ensure-state-shape planning-session-id state))
|
||||
|
||||
(defn parse-message-content
|
||||
[payload]
|
||||
(cond
|
||||
(string? payload)
|
||||
(or (try
|
||||
(some-> (js/JSON.parse payload)
|
||||
(js->clj :keywordize-keys true)
|
||||
:content
|
||||
non-empty-str)
|
||||
(catch :default _
|
||||
nil))
|
||||
(non-empty-str payload))
|
||||
|
||||
(map? payload)
|
||||
(or (some-> (:content payload) non-empty-str)
|
||||
(some-> (:message payload) non-empty-str))
|
||||
|
||||
:else
|
||||
nil))
|
||||
|
||||
(defn json-response
|
||||
[data status]
|
||||
(platform/response
|
||||
(js/JSON.stringify (clj->js data))
|
||||
#js {:status status
|
||||
:headers #js {"content-type" "application/json"}}))
|
||||
219
deps/workers/src/logseq/agents/planning_store.cljs
vendored
219
deps/workers/src/logseq/agents/planning_store.cljs
vendored
@@ -1,219 +0,0 @@
|
||||
(ns logseq.agents.planning-store
|
||||
(:require [clojure.string :as string]
|
||||
[logseq.sync.common :as common]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn- non-empty-str
|
||||
[value]
|
||||
(when (string? value)
|
||||
(let [trimmed (string/trim value)]
|
||||
(when-not (string/blank? trimmed)
|
||||
trimmed))))
|
||||
|
||||
(defn- normalize-int
|
||||
[value default-value]
|
||||
(let [parsed (if (number? value)
|
||||
value
|
||||
(some-> value str js/parseInt))]
|
||||
(if (and (number? parsed)
|
||||
(not (js/isNaN parsed)))
|
||||
parsed
|
||||
default-value)))
|
||||
|
||||
(defn- bool->int
|
||||
[value]
|
||||
(if (true? value) 1 0))
|
||||
|
||||
(defn- int->bool
|
||||
[value]
|
||||
(pos? (normalize-int value 0)))
|
||||
|
||||
(defn- parse-json
|
||||
[value default-value]
|
||||
(if-let [raw (non-empty-str value)]
|
||||
(try
|
||||
(js->clj (js/JSON.parse raw) :keywordize-keys true)
|
||||
(catch :default _
|
||||
default-value))
|
||||
default-value))
|
||||
|
||||
(defn- serialize-json
|
||||
[value]
|
||||
(when (some? value)
|
||||
(js/JSON.stringify (clj->js value))))
|
||||
|
||||
(defn- db-binding
|
||||
[^js env]
|
||||
(aget env "AGENTS_DB"))
|
||||
|
||||
(defn available?
|
||||
[^js env]
|
||||
(boolean (db-binding env)))
|
||||
|
||||
(declare <get-planning-session!)
|
||||
(declare <get-planning-session-by-id!)
|
||||
|
||||
(defn- row->planning-session
|
||||
[row]
|
||||
(when row
|
||||
(let [planning-session-id (aget row "planning_session_id")
|
||||
user-id (aget row "user_id")
|
||||
workflow-id (non-empty-str (aget row "workflow_id"))
|
||||
status (or (non-empty-str (aget row "status")) "queued")
|
||||
approval-status (or (non-empty-str (aget row "approval_status")) "pending")
|
||||
require-approval (int->bool (aget row "require_approval"))
|
||||
auto-dispatch (int->bool (aget row "auto_dispatch"))
|
||||
auto-replan (int->bool (aget row "auto_replan"))
|
||||
replan-delay-sec (normalize-int (aget row "replan_delay_sec") 0)
|
||||
created-at (normalize-int (aget row "created_at") 0)
|
||||
updated-at (normalize-int (aget row "updated_at") 0)
|
||||
last-error (non-empty-str (aget row "last_error"))
|
||||
goal (parse-json (aget row "goal_json") nil)
|
||||
plan (parse-json (aget row "plan_json") nil)
|
||||
project (parse-json (aget row "project_json") nil)
|
||||
agent (parse-json (aget row "agent_json") nil)
|
||||
scheduled-actions (parse-json (aget row "scheduled_actions_json") [])
|
||||
dispatch-sessions (parse-json (aget row "dispatch_sessions_json") [])]
|
||||
(when (and (string? planning-session-id)
|
||||
(string? user-id))
|
||||
(cond-> {:planning-session-id planning-session-id
|
||||
:user-id user-id
|
||||
:status status
|
||||
:approval-status approval-status
|
||||
:require-approval require-approval
|
||||
:auto-dispatch auto-dispatch
|
||||
:auto-replan auto-replan
|
||||
:replan-delay-sec replan-delay-sec
|
||||
:scheduled-actions (if (sequential? scheduled-actions)
|
||||
(vec scheduled-actions)
|
||||
[])
|
||||
:dispatch-sessions (if (sequential? dispatch-sessions)
|
||||
(vec dispatch-sessions)
|
||||
[])
|
||||
:created-at created-at
|
||||
:updated-at updated-at}
|
||||
(string? workflow-id) (assoc :workflow-id workflow-id)
|
||||
(map? goal) (assoc :goal goal)
|
||||
(map? plan) (assoc :plan plan)
|
||||
(map? project) (assoc :project project)
|
||||
(some? agent) (assoc :agent agent)
|
||||
(string? last-error) (assoc :last-error last-error))))))
|
||||
|
||||
(defn <upsert-planning-session!
|
||||
[^js env planning-session]
|
||||
(if-let [db (db-binding env)]
|
||||
(let [planning-session-id (some-> (:planning-session-id planning-session) non-empty-str)
|
||||
user-id (some-> (:user-id planning-session) non-empty-str)
|
||||
workflow-id (some-> (:workflow-id planning-session) non-empty-str)
|
||||
status (or (some-> (:status planning-session) non-empty-str) "queued")
|
||||
approval-status (or (some-> (:approval-status planning-session) non-empty-str)
|
||||
(if (true? (:require-approval planning-session)) "pending" "approved"))
|
||||
require-approval (bool->int (true? (:require-approval planning-session)))
|
||||
auto-dispatch (bool->int (if (boolean? (:auto-dispatch planning-session))
|
||||
(:auto-dispatch planning-session)
|
||||
true))
|
||||
auto-replan (bool->int (true? (:auto-replan planning-session)))
|
||||
replan-delay-sec (max 0 (normalize-int (:replan-delay-sec planning-session) 0))
|
||||
now (common/now-ms)]
|
||||
(if (and (string? planning-session-id)
|
||||
(string? user-id))
|
||||
(p/let [_ (common/<d1-run db
|
||||
(str "insert into planning_sessions "
|
||||
"(planning_session_id, user_id, workflow_id, status, goal_json, plan_json, project_json, agent_json, "
|
||||
"approval_status, require_approval, auto_dispatch, auto_replan, replan_delay_sec, "
|
||||
"scheduled_actions_json, dispatch_sessions_json, last_error, created_at, updated_at) "
|
||||
"values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
|
||||
"on conflict(planning_session_id) do update set "
|
||||
"user_id = excluded.user_id, "
|
||||
"workflow_id = excluded.workflow_id, "
|
||||
"status = excluded.status, "
|
||||
"goal_json = excluded.goal_json, "
|
||||
"plan_json = excluded.plan_json, "
|
||||
"project_json = excluded.project_json, "
|
||||
"agent_json = excluded.agent_json, "
|
||||
"approval_status = excluded.approval_status, "
|
||||
"require_approval = excluded.require_approval, "
|
||||
"auto_dispatch = excluded.auto_dispatch, "
|
||||
"auto_replan = excluded.auto_replan, "
|
||||
"replan_delay_sec = excluded.replan_delay_sec, "
|
||||
"scheduled_actions_json = excluded.scheduled_actions_json, "
|
||||
"dispatch_sessions_json = excluded.dispatch_sessions_json, "
|
||||
"last_error = excluded.last_error, "
|
||||
"updated_at = excluded.updated_at")
|
||||
planning-session-id
|
||||
user-id
|
||||
workflow-id
|
||||
status
|
||||
(serialize-json (:goal planning-session))
|
||||
(serialize-json (:plan planning-session))
|
||||
(serialize-json (:project planning-session))
|
||||
(serialize-json (:agent planning-session))
|
||||
approval-status
|
||||
require-approval
|
||||
auto-dispatch
|
||||
auto-replan
|
||||
replan-delay-sec
|
||||
(serialize-json (:scheduled-actions planning-session))
|
||||
(serialize-json (:dispatch-sessions planning-session))
|
||||
(some-> (:last-error planning-session) non-empty-str)
|
||||
now
|
||||
now)
|
||||
stored (<get-planning-session-by-id! env planning-session-id)]
|
||||
stored)
|
||||
(p/resolved nil)))
|
||||
(p/resolved nil)))
|
||||
|
||||
(defn <get-planning-session-by-id!
|
||||
[^js env planning-session-id]
|
||||
(if-let [db (db-binding env)]
|
||||
(if-let [planning-session-id (some-> planning-session-id non-empty-str)]
|
||||
(p/let [result (common/<d1-all db
|
||||
(str "select planning_session_id, user_id, workflow_id, status, goal_json, plan_json, project_json, "
|
||||
"agent_json, approval_status, require_approval, auto_dispatch, auto_replan, replan_delay_sec, "
|
||||
"scheduled_actions_json, dispatch_sessions_json, last_error, created_at, updated_at "
|
||||
"from planning_sessions where planning_session_id = ? limit 1")
|
||||
planning-session-id)
|
||||
rows (common/get-sql-rows result)]
|
||||
(row->planning-session (first rows)))
|
||||
(p/resolved nil))
|
||||
(p/resolved nil)))
|
||||
|
||||
(defn <get-planning-session!
|
||||
[^js env user-id planning-session-id]
|
||||
(if-let [db (db-binding env)]
|
||||
(if-let [user-id (some-> user-id non-empty-str)]
|
||||
(if-let [planning-session-id (some-> planning-session-id non-empty-str)]
|
||||
(p/let [result (common/<d1-all db
|
||||
(str "select planning_session_id, user_id, workflow_id, status, goal_json, plan_json, project_json, "
|
||||
"agent_json, approval_status, require_approval, auto_dispatch, auto_replan, replan_delay_sec, "
|
||||
"scheduled_actions_json, dispatch_sessions_json, last_error, created_at, updated_at "
|
||||
"from planning_sessions where planning_session_id = ? and user_id = ? limit 1")
|
||||
planning-session-id
|
||||
user-id)
|
||||
rows (common/get-sql-rows result)]
|
||||
(row->planning-session (first rows)))
|
||||
(p/resolved nil))
|
||||
(p/resolved nil))
|
||||
(p/resolved nil)))
|
||||
|
||||
(defn <update-planning-session!
|
||||
[^js env user-id planning-session-id updates]
|
||||
(p/let [existing (<get-planning-session! env user-id planning-session-id)]
|
||||
(if-not (map? existing)
|
||||
nil
|
||||
(<upsert-planning-session! env (merge existing updates)))))
|
||||
|
||||
(defn <update-planning-session-by-id!
|
||||
[^js env planning-session-id updates]
|
||||
(p/let [existing (<get-planning-session-by-id! env planning-session-id)]
|
||||
(if-not (map? existing)
|
||||
nil
|
||||
(<upsert-planning-session! env (merge existing updates)))))
|
||||
|
||||
(defn <set-planning-approval!
|
||||
[^js env user-id planning-session-id approval-status]
|
||||
(<update-planning-session! env
|
||||
user-id
|
||||
planning-session-id
|
||||
{:approval-status (or (some-> approval-status non-empty-str)
|
||||
"pending")}))
|
||||
@@ -1,931 +0,0 @@
|
||||
(ns logseq.agents.planning-workflow
|
||||
(:require [clojure.string :as string]
|
||||
[lambdaisland.glogi :as log]
|
||||
[logseq.agents.planning-store :as planning-store]
|
||||
[logseq.sync.platform.core :as platform]))
|
||||
|
||||
(defn- non-empty-str
|
||||
[value]
|
||||
(when (string? value)
|
||||
(let [trimmed (string/trim value)]
|
||||
(when-not (string/blank? trimmed)
|
||||
trimmed))))
|
||||
|
||||
(defn- task-content
|
||||
[task]
|
||||
(or (some-> (:content task) non-empty-str)
|
||||
(let [title (some-> (:title task) non-empty-str)
|
||||
description (some-> (:description task) non-empty-str)]
|
||||
(cond
|
||||
(and title description) (str title "\n" description)
|
||||
title title
|
||||
description description
|
||||
:else nil))))
|
||||
|
||||
(defn- maybe-task-uuid
|
||||
[idx task]
|
||||
(or (some-> (:task-uuid task) non-empty-str)
|
||||
(some-> (:block-uuid task) non-empty-str)
|
||||
(str "task-" (inc idx))))
|
||||
|
||||
(declare native-promise)
|
||||
|
||||
(defn normalize-task
|
||||
[idx task]
|
||||
(when (map? task)
|
||||
(when-let [content (task-content task)]
|
||||
(cond-> {:title (or (some-> (:title task) non-empty-str)
|
||||
(str "Task " (inc idx)))
|
||||
:content content}
|
||||
(some-> (:description task) non-empty-str) (assoc :description (non-empty-str (:description task)))
|
||||
(sequential? (:dependencies task)) (assoc :dependencies (vec (:dependencies task)))
|
||||
(sequential? (:acceptance-criteria task)) (assoc :acceptance-criteria (vec (:acceptance-criteria task)))))))
|
||||
|
||||
(defn- normalize-planner-task
|
||||
[idx task]
|
||||
(when-let [normalized (normalize-task idx task)]
|
||||
(cond-> (assoc normalized
|
||||
:task-uuid (maybe-task-uuid idx task)
|
||||
:status "Todo"
|
||||
:marker "#Task")
|
||||
(some-> (:block-uuid task) non-empty-str) (assoc :block-uuid (non-empty-str (:block-uuid task))))))
|
||||
|
||||
(defn normalize-tasks
|
||||
[tasks]
|
||||
(->> tasks
|
||||
(map-indexed normalize-task)
|
||||
(remove nil?)
|
||||
vec))
|
||||
|
||||
(defn- normalize-planner-tasks
|
||||
[tasks]
|
||||
(->> tasks
|
||||
(map-indexed normalize-planner-task)
|
||||
(remove nil?)
|
||||
vec))
|
||||
|
||||
(defn- fallback-task-title
|
||||
[goal fallback]
|
||||
(or (some-> goal :title non-empty-str)
|
||||
(some-> goal :node-title non-empty-str)
|
||||
fallback))
|
||||
|
||||
(defn- planning-message-text
|
||||
[message]
|
||||
(cond
|
||||
(string? message)
|
||||
(non-empty-str message)
|
||||
|
||||
(map? message)
|
||||
(or (some-> (:content message) non-empty-str)
|
||||
(some-> (:message message) non-empty-str)
|
||||
(some-> (:text message) non-empty-str))
|
||||
|
||||
:else
|
||||
nil))
|
||||
|
||||
(defn- planning-message-texts
|
||||
[params]
|
||||
(->> (:planning-messages params)
|
||||
(keep planning-message-text)
|
||||
vec))
|
||||
|
||||
(defn- repo-note
|
||||
[params]
|
||||
(let [repo-url (some-> (get-in params [:project :repo-url]) non-empty-str)
|
||||
base-branch (some-> (get-in params [:project :base-branch]) non-empty-str)]
|
||||
(when (string? repo-url)
|
||||
(str "Repository: " repo-url
|
||||
(when (string? base-branch)
|
||||
(str " (base branch: " base-branch ")"))))))
|
||||
|
||||
(defn- planning-context-texts
|
||||
[params]
|
||||
(let [goal (:goal params)]
|
||||
(->> [(some-> goal :title non-empty-str)
|
||||
(some-> goal :description non-empty-str)
|
||||
(some-> goal :node-title non-empty-str)
|
||||
(some-> (:replan-note params) non-empty-str)
|
||||
(some-> (get-in params [:approval :comment]) non-empty-str)
|
||||
(repo-note params)]
|
||||
(concat (planning-message-texts params))
|
||||
(keep non-empty-str)
|
||||
vec)))
|
||||
|
||||
(defn- parse-json-safe
|
||||
[value]
|
||||
(if (string? value)
|
||||
(try
|
||||
(js->clj (js/JSON.parse value) :keywordize-keys true)
|
||||
(catch :default _
|
||||
nil))
|
||||
nil))
|
||||
|
||||
(defn- extract-file-references
|
||||
[params]
|
||||
(let [seen (volatile! #{})]
|
||||
(->> (planning-context-texts params)
|
||||
(mapcat #(re-seq #"[A-Za-z0-9_./-]+\.(?:clj|cljs|cljc|edn|sql|ts|tsx|js|jsx)" %))
|
||||
(keep (fn [file-path]
|
||||
(let [normalized (non-empty-str file-path)]
|
||||
(when (and (string? normalized)
|
||||
(not (contains? @seen normalized)))
|
||||
(vswap! seen conj normalized)
|
||||
normalized))))
|
||||
vec)))
|
||||
|
||||
(defn- planner-api-token
|
||||
[^js env params]
|
||||
(or (some-> (get-in params [:agent :api-token]) non-empty-str)
|
||||
(some-> (aget env "OPENAI_API_KEY") non-empty-str)))
|
||||
|
||||
(defn- planner-base-url
|
||||
[^js env params]
|
||||
(let [base-url (or (some-> (get-in params [:agent :base-url]) non-empty-str)
|
||||
(some-> (aget env "OPENAI_BASE_URL") non-empty-str)
|
||||
"https://api.openai.com/v1")]
|
||||
(string/replace base-url #"/+$" "")))
|
||||
|
||||
(defn- planner-model
|
||||
[params]
|
||||
(or (some-> (get-in params [:agent :model]) non-empty-str)
|
||||
"gpt-4.1-mini"))
|
||||
|
||||
(defn- generic-clarifications
|
||||
[params]
|
||||
(let [goal (:goal params)
|
||||
goal-description (or (some-> goal :description non-empty-str)
|
||||
(some-> goal :title non-empty-str)
|
||||
(some-> goal :node-title non-empty-str))
|
||||
repo-url (some-> (get-in params [:project :repo-url]) non-empty-str)
|
||||
replan-note (some-> (:replan-note params) non-empty-str)]
|
||||
(vec
|
||||
(keep identity
|
||||
[(when-not (string? repo-url)
|
||||
"Confirm the target repository and branch before final implementation work begins.")
|
||||
(when (and (string? goal-description)
|
||||
(< (count goal-description) 48))
|
||||
"Clarify the expected user-visible outcome so the plan can stay focused.")
|
||||
(when (string? replan-note)
|
||||
(str "Address the replanning feedback: " replan-note))]))))
|
||||
|
||||
(defn- generic-workstreams
|
||||
[params tasks]
|
||||
(let [file-references (extract-file-references params)]
|
||||
(cond
|
||||
(seq tasks)
|
||||
(->> tasks
|
||||
(take 4)
|
||||
(mapv (fn [task]
|
||||
{:title (or (some-> (:title task) non-empty-str)
|
||||
"Implementation task")
|
||||
:description (or (some-> (:description task) non-empty-str)
|
||||
(task-content task)
|
||||
"Complete the planned workstream.")})))
|
||||
|
||||
(seq file-references)
|
||||
[{:title "Codebase review"
|
||||
:description (str "Review the referenced files and current implementation constraints: "
|
||||
(string/join ", " file-references)
|
||||
".")}
|
||||
{:title "Implementation"
|
||||
:description "Translate the requested change into concrete code and data-flow updates."}
|
||||
{:title "Validation"
|
||||
:description "Check the result end-to-end and summarize any follow-up work."}]
|
||||
|
||||
:else
|
||||
[{:title "Investigation"
|
||||
:description "Review the current state, surrounding constraints, and open questions."}
|
||||
{:title "Implementation"
|
||||
:description "Translate the request into concrete implementation tasks."}
|
||||
{:title "Validation"
|
||||
:description "Verify the outcome and capture remaining risks."}])))
|
||||
|
||||
(defn- generic-milestones
|
||||
[workstreams]
|
||||
(mapv (fn [workstream]
|
||||
{:title (str (:title workstream) " completed")
|
||||
:description (:description workstream)})
|
||||
workstreams))
|
||||
|
||||
(defn- generic-dependencies
|
||||
[params]
|
||||
(vec
|
||||
(keep identity
|
||||
[(when-let [repo (repo-note params)]
|
||||
(str "Use " repo " as the source of truth for planning and validation."))
|
||||
(when (true? (:require-approval params))
|
||||
"Execution should remain blocked until the approval state is explicitly updated.")])))
|
||||
|
||||
(defn- generic-risks
|
||||
[params]
|
||||
(vec
|
||||
(keep identity
|
||||
[(when (true? (:require-approval params))
|
||||
"Approval and execution state can drift if the planning UI is not refreshed after control actions.")
|
||||
(when-not (string? (some-> (get-in params [:project :repo-url]) non-empty-str))
|
||||
"Repository-aware planning will stay shallow until repo metadata is available.")])))
|
||||
|
||||
(defn- generic-planner-tasks
|
||||
[params]
|
||||
(let [goal (:goal params)
|
||||
goal-description (or (some-> goal :description non-empty-str)
|
||||
(some-> goal :title non-empty-str)
|
||||
(some-> goal :node-title non-empty-str)
|
||||
"Complete the requested work.")
|
||||
file-references (extract-file-references params)]
|
||||
[{:title "Inspect current state"
|
||||
:description (str "Review the goal, current codebase, and constraints. "
|
||||
goal-description
|
||||
(when-let [repo (repo-note params)]
|
||||
(str "\n" repo))
|
||||
(when (seq file-references)
|
||||
(str "\nReferenced files: " (string/join ", " file-references))))}
|
||||
{:title (str "Implement " (fallback-task-title goal "the requested change"))
|
||||
:description goal-description}
|
||||
{:title "Validate and summarize"
|
||||
:description "Check the result, note risks, and summarize follow-up work."}]))
|
||||
|
||||
(def ^:private planner-json-schema
|
||||
{:type "object"
|
||||
:additionalProperties false
|
||||
:required ["goal_understanding" "clarifications" "workstreams" "milestones" "tasks" "dependencies" "risks"]
|
||||
:properties {"goal_understanding" {:type "string"}
|
||||
"clarifications" {:type "array"
|
||||
:items {:type "string"}}
|
||||
"workstreams" {:type "array"
|
||||
:items {:type "object"
|
||||
:additionalProperties false
|
||||
:required ["title" "description"]
|
||||
:properties {"title" {:type "string"}
|
||||
"description" {:type "string"}}}}
|
||||
"milestones" {:type "array"
|
||||
:items {:type "object"
|
||||
:additionalProperties false
|
||||
:required ["title" "description"]
|
||||
:properties {"title" {:type "string"}
|
||||
"description" {:type "string"}}}}
|
||||
"tasks" {:type "array"
|
||||
:items {:type "object"
|
||||
:additionalProperties false
|
||||
:required ["title" "description"]
|
||||
:properties {"title" {:type "string"}
|
||||
"description" {:type "string"}
|
||||
"content" {:type "string"}
|
||||
"task_uuid" {:type "string"}
|
||||
"block_uuid" {:type "string"}
|
||||
"dependencies" {:type "array"
|
||||
:items {:type "string"}}
|
||||
"acceptance_criteria" {:type "array"
|
||||
:items {:type "string"}}}}}
|
||||
"dependencies" {:type "array"
|
||||
:items {:type "string"}}
|
||||
"risks" {:type "array"
|
||||
:items {:type "string"}}}})
|
||||
|
||||
(defn- planner-prompt
|
||||
[params]
|
||||
(let [goal (:goal params)
|
||||
existing-tasks (or (:tasks params)
|
||||
(:planned-tasks params)
|
||||
(:existing-tasks params)
|
||||
[])
|
||||
context {:goal {:title (some-> goal :title non-empty-str)
|
||||
:description (or (some-> goal :description non-empty-str)
|
||||
(some-> goal :node-title non-empty-str))
|
||||
:node-title (some-> goal :node-title non-empty-str)}
|
||||
:project {:repo-url (some-> (get-in params [:project :repo-url]) non-empty-str)
|
||||
:base-branch (some-> (get-in params [:project :base-branch]) non-empty-str)}
|
||||
:planning-messages (planning-message-texts params)
|
||||
:existing-tasks (->> existing-tasks
|
||||
(keep #(select-keys % [:task-uuid :block-uuid :title :description :content :status :session-id]))
|
||||
vec)
|
||||
:require-approval (true? (:require-approval params))
|
||||
:auto-dispatch (if (boolean? (:auto-dispatch params))
|
||||
(:auto-dispatch params)
|
||||
true)
|
||||
:replan-note (some-> (:replan-note params) non-empty-str)}]
|
||||
(str "You are a software planning model. Produce a concise implementation plan as strict JSON.\n"
|
||||
"Do not anchor on technology or file names unless they are present in the provided context.\n"
|
||||
"Use repo metadata and planning messages only as supporting context.\n"
|
||||
"Return implementation-oriented workstreams, milestones, tasks, dependencies, and risks.\n\n"
|
||||
"Planning context JSON:\n"
|
||||
(js/JSON.stringify (clj->js context) nil 2))))
|
||||
|
||||
(defn- normalize-model-plan
|
||||
[payload params]
|
||||
(when (map? payload)
|
||||
(let [tasks (normalize-tasks (:tasks payload))
|
||||
workstreams (if (sequential? (:workstreams payload))
|
||||
(->> (:workstreams payload)
|
||||
(keep #(when (map? %)
|
||||
(select-keys % [:title :description])))
|
||||
vec)
|
||||
[])
|
||||
milestones (if (sequential? (:milestones payload))
|
||||
(->> (:milestones payload)
|
||||
(keep #(when (map? %)
|
||||
(select-keys % [:title :description])))
|
||||
vec)
|
||||
[])]
|
||||
(when (seq tasks)
|
||||
{:goal-understanding (or (some-> (:goal_understanding payload) non-empty-str)
|
||||
(some-> (:goal-understanding payload) non-empty-str)
|
||||
(some-> (:goal params) :description non-empty-str)
|
||||
(some-> (:goal params) :title non-empty-str)
|
||||
"")
|
||||
:clarifications (if (sequential? (:clarifications payload))
|
||||
(vec (keep non-empty-str (:clarifications payload)))
|
||||
[])
|
||||
:workstreams workstreams
|
||||
:milestones milestones
|
||||
:tasks tasks
|
||||
:dependencies (if (sequential? (:dependencies payload))
|
||||
(vec (keep non-empty-str (:dependencies payload)))
|
||||
[])
|
||||
:risks (if (sequential? (:risks payload))
|
||||
(vec (keep non-empty-str (:risks payload)))
|
||||
[])}))))
|
||||
|
||||
(defn- <planner-model-plan!
|
||||
[^js env params]
|
||||
(if-let [token (planner-api-token env params)]
|
||||
(let [headers (js/Headers.)
|
||||
_ (.set headers "content-type" "application/json")
|
||||
_ (.set headers "authorization" (str "Bearer " token))
|
||||
request-body {:model (planner-model params)
|
||||
:messages [{:role "system"
|
||||
:content "You are a planning model that produces structured implementation plans for software tasks."}
|
||||
{:role "user"
|
||||
:content (planner-prompt params)}]
|
||||
:response_format {:type "json_schema"
|
||||
:json_schema {:name "planning_plan"
|
||||
:strict true
|
||||
:schema planner-json-schema}}}
|
||||
url (str (planner-base-url env params) "/chat/completions")]
|
||||
(-> (js/fetch url
|
||||
#js {:method "POST"
|
||||
:headers headers
|
||||
:body (js/JSON.stringify (clj->js request-body))})
|
||||
(.then (fn [response]
|
||||
(if-not (.-ok response)
|
||||
nil
|
||||
(-> (.json response)
|
||||
(.then (fn [payload]
|
||||
(let [payload (js->clj payload :keywordize-keys true)
|
||||
content (some-> payload :choices first :message :content non-empty-str)
|
||||
plan-payload (or (parse-json-safe content)
|
||||
(some-> payload :choices first :message :parsed))]
|
||||
(normalize-model-plan plan-payload params))))))))
|
||||
(.catch (fn [error]
|
||||
(log/warn :agent/planning-model-plan-failed error)
|
||||
nil))))
|
||||
(native-promise nil)))
|
||||
|
||||
(defn- <with-timeout
|
||||
[promise timeout-ms timeout-value]
|
||||
(if-not (and (number? timeout-ms)
|
||||
(pos? timeout-ms))
|
||||
promise
|
||||
(js/Promise.race
|
||||
#js [promise
|
||||
(js/Promise.
|
||||
(fn [resolve _reject]
|
||||
(js/setTimeout
|
||||
(fn [] (resolve timeout-value))
|
||||
timeout-ms)))])))
|
||||
|
||||
(defn enrich-params-with-model-plan
|
||||
([^js env params]
|
||||
(enrich-params-with-model-plan env params nil))
|
||||
([^js env params {:keys [timeout-ms]}]
|
||||
(-> (<planner-model-plan! env params)
|
||||
(<with-timeout timeout-ms nil)
|
||||
(.then (fn [plan]
|
||||
(if (map? plan)
|
||||
(assoc params
|
||||
:plan plan
|
||||
:tasks (:tasks plan))
|
||||
params))))))
|
||||
|
||||
(defn- planner-source-tasks
|
||||
[params]
|
||||
(let [tasks (or (:tasks params) (:planned-tasks params) (some-> params :plan :tasks))]
|
||||
(if (seq tasks)
|
||||
tasks
|
||||
(generic-planner-tasks params))))
|
||||
|
||||
(defn build-plan
|
||||
[params]
|
||||
(let [goal (:goal params)
|
||||
tasks (normalize-tasks (planner-source-tasks params))
|
||||
existing-plan (when (map? (:plan params)) (:plan params))
|
||||
workstreams (or (some-> existing-plan :workstreams seq vec)
|
||||
(generic-workstreams params tasks))]
|
||||
{:goal-understanding (or (some-> existing-plan :goal-understanding non-empty-str)
|
||||
(some-> goal :description non-empty-str)
|
||||
(some-> goal :title non-empty-str)
|
||||
(some-> goal :node-title non-empty-str)
|
||||
"")
|
||||
:clarifications (or (some-> existing-plan :clarifications seq vec)
|
||||
(generic-clarifications params))
|
||||
:workstreams workstreams
|
||||
:milestones (or (some-> existing-plan :milestones seq vec)
|
||||
(generic-milestones workstreams))
|
||||
:tasks tasks
|
||||
:dependencies (or (some-> existing-plan :dependencies seq vec)
|
||||
(generic-dependencies params))
|
||||
:risks (or (some-> existing-plan :risks seq vec)
|
||||
(generic-risks params))}))
|
||||
|
||||
(defn- approval-decision
|
||||
[params]
|
||||
(let [decision (some-> (get-in params [:approval :decision])
|
||||
non-empty-str
|
||||
string/lower-case)]
|
||||
(cond
|
||||
(contains? #{"approved" "pending" "rejected"} decision)
|
||||
decision
|
||||
|
||||
(true? (:require-approval params))
|
||||
"pending"
|
||||
|
||||
:else
|
||||
"approved")))
|
||||
|
||||
(defn- started-task?
|
||||
[task]
|
||||
(or (string? (some-> (:session-id task) non-empty-str))
|
||||
(contains? #{"Doing" "Done" "In Review" "Canceled"}
|
||||
(some-> (:status task) non-empty-str))))
|
||||
|
||||
(def ^:private execution-owned-fields
|
||||
#{:session-id
|
||||
:status
|
||||
:runtime-provider
|
||||
:runner-id
|
||||
:pr-url
|
||||
:runtime
|
||||
:sandbox-checkpoint
|
||||
:checkpoint-metadata})
|
||||
|
||||
(defn- preserve-execution-owned-fields
|
||||
[existing planned]
|
||||
(merge planned
|
||||
(select-keys existing execution-owned-fields)
|
||||
(select-keys existing [:title :content :description :block-uuid])))
|
||||
|
||||
(defn- task-match
|
||||
[existing task]
|
||||
(let [task-uuid (some-> (:task-uuid task) non-empty-str)]
|
||||
(some (fn [candidate]
|
||||
(when (= task-uuid (some-> (:task-uuid candidate) non-empty-str))
|
||||
candidate))
|
||||
existing)))
|
||||
|
||||
(defn- reconcile-planned-tasks
|
||||
[existing-tasks planned-tasks]
|
||||
(let [existing (vec (filter map? existing-tasks))
|
||||
matched-task-uuids (volatile! #{})
|
||||
reconciled (mapv (fn [task]
|
||||
(if-let [matched (task-match existing task)]
|
||||
(do
|
||||
(when-let [task-uuid (some-> (:task-uuid matched) non-empty-str)]
|
||||
(vswap! matched-task-uuids conj task-uuid))
|
||||
(if (started-task? matched)
|
||||
(preserve-execution-owned-fields matched task)
|
||||
(merge matched task)))
|
||||
task))
|
||||
planned-tasks)
|
||||
untouched-existing (->> existing
|
||||
(remove (fn [task]
|
||||
(contains? @matched-task-uuids
|
||||
(or (some-> (:task-uuid task) non-empty-str)
|
||||
""))))
|
||||
(remove nil?)
|
||||
vec)]
|
||||
(vec (concat reconciled untouched-existing))))
|
||||
|
||||
(defn- planning-status
|
||||
[approval-status dispatch-sessions auto-dispatch?]
|
||||
(cond
|
||||
(= "pending" approval-status) "waiting-approval"
|
||||
(= "rejected" approval-status) "rejected"
|
||||
(and auto-dispatch? (seq dispatch-sessions)) "dispatching"
|
||||
:else "planned"))
|
||||
|
||||
(defn- dispatch-session-id
|
||||
[planning-session-id task]
|
||||
(or (some-> (:session-id task) non-empty-str)
|
||||
(some-> (:goal task) :node-id non-empty-str)
|
||||
(some-> (:goal task) :block-uuid non-empty-str)
|
||||
(some-> (:goal task) :id non-empty-str)
|
||||
(some-> task :parent-session-id non-empty-str)
|
||||
(some-> planning-session-id non-empty-str)))
|
||||
|
||||
(defn- dispatch-session
|
||||
[planning-session-id params task]
|
||||
(let [runtime-provider (some-> (:runtime-provider params) non-empty-str string/lower-case)
|
||||
runner-id (some-> (:runner-id params) non-empty-str)
|
||||
parent-session-id (or (some-> (:execution-session-id params) non-empty-str)
|
||||
(some-> (:goal params) :node-id non-empty-str)
|
||||
(some-> (:goal params) :block-uuid non-empty-str))
|
||||
goal-title (or (some-> (:goal params) :title non-empty-str)
|
||||
(some-> (:goal params) :node-title non-empty-str)
|
||||
"Planning Task")]
|
||||
(cond-> {:id (dispatch-session-id planning-session-id
|
||||
(assoc task :parent-session-id parent-session-id))
|
||||
:source {:node-id planning-session-id
|
||||
:node-title goal-title}
|
||||
:intent {:content (:content task)}
|
||||
:project (:project params)
|
||||
:agent (:agent params)
|
||||
:task-uuid (:task-uuid task)
|
||||
:capabilities {:push-enabled true
|
||||
:pr-enabled true}}
|
||||
(string? runtime-provider) (assoc :runtime-provider runtime-provider)
|
||||
(string? runner-id) (assoc :runner-id runner-id))))
|
||||
|
||||
(defn- ready-dispatch-task?
|
||||
[task]
|
||||
(not (started-task? task)))
|
||||
|
||||
(defn- next-dispatchable-tasks
|
||||
[planning-session-id params tasks]
|
||||
(if-let [task (some ready-dispatch-task? tasks)]
|
||||
[(dispatch-session planning-session-id params task)]
|
||||
[]))
|
||||
|
||||
(defn- repo-aware-state
|
||||
[params]
|
||||
{:enabled (boolean (some-> (get-in params [:project :repo-url]) non-empty-str))
|
||||
:repo-url (some-> (get-in params [:project :repo-url]) non-empty-str)
|
||||
:base-branch (some-> (get-in params [:project :base-branch]) non-empty-str)
|
||||
:agent-provider (or (some-> (:agent params) :provider non-empty-str)
|
||||
(some-> (:agent params) non-empty-str))})
|
||||
|
||||
(defn- scheduled-actions
|
||||
[dispatch-ready? dispatch-sessions auto-replan? replan-delay-sec]
|
||||
(if (and dispatch-ready?
|
||||
(seq dispatch-sessions)
|
||||
auto-replan?
|
||||
(pos-int? replan-delay-sec))
|
||||
[{:type "replan"
|
||||
:delay-sec replan-delay-sec}]
|
||||
[]))
|
||||
|
||||
(defn- session-stub
|
||||
[^js env session-id]
|
||||
(when-let [^js namespace (aget env "LOGSEQ_AGENT_SESSION_DO")]
|
||||
(let [do-id (.idFromName namespace session-id)]
|
||||
(.get namespace do-id))))
|
||||
|
||||
(defn- <dispatch-session!
|
||||
[^js env user-id session-task]
|
||||
(if-let [^js stub (session-stub env (:id session-task))]
|
||||
(let [headers (js/Headers.)]
|
||||
(.set headers "content-type" "application/json")
|
||||
(when (string? user-id)
|
||||
(.set headers "x-user-id" user-id))
|
||||
(let [request (platform/request "https://planning.internal/__session__/init"
|
||||
#js {:method "POST"
|
||||
:headers headers
|
||||
:body (js/JSON.stringify (clj->js session-task))})]
|
||||
(-> (.fetch stub request)
|
||||
(.then (fn [response]
|
||||
{:session-id (:id session-task)
|
||||
:ok (.-ok response)
|
||||
:status (.-status response)}))
|
||||
(.catch (fn [error]
|
||||
{:session-id (:id session-task)
|
||||
:ok false
|
||||
:status 500
|
||||
:error (str error)})))))
|
||||
(native-promise {:session-id (:id session-task)
|
||||
:ok false
|
||||
:status 503
|
||||
:error "session durable object unavailable"})))
|
||||
|
||||
(defn- <dispatch-sessions!
|
||||
[^js env user-id dispatch-sessions]
|
||||
(js/Promise.all
|
||||
(clj->js
|
||||
(mapv (fn [session-task]
|
||||
(<dispatch-session! env user-id session-task))
|
||||
dispatch-sessions))))
|
||||
|
||||
(defn- orchestrate
|
||||
[params]
|
||||
(let [planning-session-id (or (some-> (:planning-session-id params) non-empty-str)
|
||||
(str (random-uuid)))
|
||||
planned-tasks (normalize-planner-tasks (planner-source-tasks params))
|
||||
existing-tasks (if (sequential? (:existing-tasks params))
|
||||
(vec (:existing-tasks params))
|
||||
[])
|
||||
reconciled-tasks (reconcile-planned-tasks existing-tasks planned-tasks)
|
||||
approval-status (approval-decision params)
|
||||
auto-dispatch? (if (boolean? (:auto-dispatch params))
|
||||
(:auto-dispatch params)
|
||||
true)
|
||||
auto-replan? (true? (:auto-replan params))
|
||||
replan-delay-sec (let [delay (if (number? (:replan-delay-sec params))
|
||||
(:replan-delay-sec params)
|
||||
(some-> (:replan-delay-sec params) str js/parseInt))]
|
||||
(if (and (number? delay) (not (js/isNaN delay)))
|
||||
(max 0 delay)
|
||||
0))
|
||||
dispatch-ready? (and auto-dispatch?
|
||||
(= "approved" approval-status))
|
||||
dispatch-sessions (if dispatch-ready?
|
||||
(next-dispatchable-tasks planning-session-id params reconciled-tasks)
|
||||
[])
|
||||
status (planning-status approval-status dispatch-sessions auto-dispatch?)
|
||||
plan (build-plan params)]
|
||||
{:ok true
|
||||
:status status
|
||||
:planning-session-id planning-session-id
|
||||
:plan plan
|
||||
:reconciled-tasks reconciled-tasks
|
||||
:dispatch-sessions dispatch-sessions
|
||||
:scheduled-actions (scheduled-actions dispatch-ready? dispatch-sessions auto-replan? replan-delay-sec)
|
||||
:planning-state {:planning-session-id planning-session-id
|
||||
:phase (if (= "waiting-approval" status) "approval" "planning")
|
||||
:approval-status approval-status
|
||||
:requires-approval (true? (:require-approval params))
|
||||
:auto-dispatch auto-dispatch?
|
||||
:auto-replan auto-replan?
|
||||
:replan-delay-sec replan-delay-sec
|
||||
:repo-aware (repo-aware-state params)}
|
||||
:goal (:goal params)
|
||||
:require-approval (true? (:require-approval params))}))
|
||||
|
||||
(defn workflow-response
|
||||
[instance details]
|
||||
{:workflow-id (or (some-> instance .-id)
|
||||
(:id details))
|
||||
:status (or (:status details)
|
||||
"queued")
|
||||
:details details})
|
||||
|
||||
(defn orchestrate-response
|
||||
[params]
|
||||
(orchestrate params))
|
||||
|
||||
(defn- native-promise
|
||||
[value]
|
||||
(js/Promise.resolve value))
|
||||
|
||||
(defn- <run-step-do
|
||||
[step step-name f]
|
||||
(let [step-do (when step (aget step "do"))]
|
||||
(if (fn? step-do)
|
||||
(.call step-do
|
||||
step
|
||||
step-name
|
||||
(fn []
|
||||
(native-promise (f))))
|
||||
(native-promise (f)))))
|
||||
|
||||
(defn- <dispatch-sessions-with-step!
|
||||
[^js env user-id dispatch-sessions step step-name]
|
||||
(if (and (seq dispatch-sessions)
|
||||
(string? user-id)
|
||||
env)
|
||||
(<run-step-do step
|
||||
step-name
|
||||
(fn []
|
||||
(<dispatch-sessions! env user-id dispatch-sessions)))
|
||||
(native-promise nil)))
|
||||
|
||||
(defn- with-dispatched-task-sessions
|
||||
[orchestrated]
|
||||
(let [dispatch-sessions (:dispatch-sessions orchestrated)
|
||||
session-id-by-task (->> dispatch-sessions
|
||||
(reduce (fn [acc session]
|
||||
(if-let [task-uuid (some-> (:task-uuid session) non-empty-str)]
|
||||
(assoc acc task-uuid (:id session))
|
||||
acc))
|
||||
{}))]
|
||||
(if (empty? session-id-by-task)
|
||||
orchestrated
|
||||
(update orchestrated
|
||||
:reconciled-tasks
|
||||
(fn [tasks]
|
||||
(mapv (fn [task]
|
||||
(if-let [session-id (get session-id-by-task
|
||||
(some-> (:task-uuid task) non-empty-str))]
|
||||
(cond-> (assoc task :session-id session-id)
|
||||
(not (started-task? task)) (assoc :status "Doing"))
|
||||
task))
|
||||
(or tasks [])))))))
|
||||
|
||||
(defn- workflow-event-payload
|
||||
[event]
|
||||
(let [event (if (map? event)
|
||||
event
|
||||
(js->clj event :keywordize-keys true))]
|
||||
(when (map? event)
|
||||
(if (contains? event :payload)
|
||||
(:payload event)
|
||||
event))))
|
||||
|
||||
(defn- approval-decision-from-event
|
||||
[event]
|
||||
(let [payload (workflow-event-payload event)
|
||||
decision (some-> (:decision payload) non-empty-str string/lower-case)]
|
||||
(cond
|
||||
(contains? #{"approved" "pending" "rejected"} decision)
|
||||
decision
|
||||
|
||||
(true? (:approved payload))
|
||||
"approved"
|
||||
|
||||
(false? (:approved payload))
|
||||
"rejected"
|
||||
|
||||
:else
|
||||
"pending")))
|
||||
|
||||
(defn- <wait-for-approval-event!
|
||||
[step]
|
||||
(let [wait-for-event (when step (aget step "waitForEvent"))]
|
||||
(if (fn? wait-for-event)
|
||||
(-> (.call wait-for-event
|
||||
step
|
||||
"wait-for-approval"
|
||||
#js {:type "approval"
|
||||
:timeout "30 days"})
|
||||
(.then approval-decision-from-event)
|
||||
(.catch (fn [_]
|
||||
"pending")))
|
||||
(js/Promise.resolve "pending"))))
|
||||
|
||||
(defn- replan-action
|
||||
[orchestrated]
|
||||
(->> (:scheduled-actions orchestrated)
|
||||
(filter map?)
|
||||
(some (fn [action]
|
||||
(when (= "replan" (some-> (:type action) non-empty-str string/lower-case))
|
||||
action)))))
|
||||
|
||||
(defn- <sleep-for-replan!
|
||||
[step delay-sec]
|
||||
(let [delay-sec (if (and (number? delay-sec)
|
||||
(not (js/isNaN delay-sec)))
|
||||
(max 0 delay-sec)
|
||||
0)
|
||||
step-sleep (when step (aget step "sleep"))]
|
||||
(cond
|
||||
(and (pos-int? delay-sec)
|
||||
(fn? step-sleep))
|
||||
(.call step-sleep
|
||||
step
|
||||
(str "scheduled-replan-delay-" delay-sec)
|
||||
(str delay-sec " seconds"))
|
||||
|
||||
(pos-int? delay-sec)
|
||||
(js/Promise.
|
||||
(fn [resolve _reject]
|
||||
(js/setTimeout resolve (* delay-sec 1000))))
|
||||
|
||||
:else
|
||||
(js/Promise.resolve nil))))
|
||||
|
||||
(defn- <persist-planning-session!
|
||||
[^js env params orchestrated]
|
||||
(let [user-id (some-> (:user-id params) non-empty-str)
|
||||
planning-session-id (some-> (:planning-session-id orchestrated) non-empty-str)
|
||||
workflow-id (some-> (:workflow-id params) non-empty-str)
|
||||
persisted-plan (assoc (:plan orchestrated)
|
||||
:tasks (:reconciled-tasks orchestrated))]
|
||||
(if (and env
|
||||
(planning-store/available? env)
|
||||
(string? user-id)
|
||||
(string? planning-session-id))
|
||||
(native-promise
|
||||
(planning-store/<upsert-planning-session! env
|
||||
{:planning-session-id planning-session-id
|
||||
:user-id user-id
|
||||
:workflow-id workflow-id
|
||||
:status (:status orchestrated)
|
||||
:goal (:goal params)
|
||||
:project (:project params)
|
||||
:agent (:agent params)
|
||||
:plan persisted-plan
|
||||
:approval-status (get-in orchestrated [:planning-state :approval-status])
|
||||
:require-approval (true? (:require-approval params))
|
||||
:auto-dispatch (if (boolean? (:auto-dispatch params))
|
||||
(:auto-dispatch params)
|
||||
true)
|
||||
:auto-replan (true? (:auto-replan params))
|
||||
:replan-delay-sec (:replan-delay-sec params)
|
||||
:scheduled-actions (:scheduled-actions orchestrated)
|
||||
:dispatch-sessions (:dispatch-sessions orchestrated)}))
|
||||
(native-promise nil))))
|
||||
|
||||
(defn- replan-event-params
|
||||
[event]
|
||||
(let [payload (workflow-event-payload event)
|
||||
params (:params payload)]
|
||||
(when (map? params)
|
||||
params)))
|
||||
|
||||
(defn- <wait-for-replan-event!
|
||||
[step]
|
||||
(let [wait-for-event (when step (aget step "waitForEvent"))]
|
||||
(if (fn? wait-for-event)
|
||||
(-> (.call wait-for-event
|
||||
step
|
||||
"wait-for-replan"
|
||||
#js {:type "replan"
|
||||
:timeout "30 days"})
|
||||
(.then replan-event-params)
|
||||
(.catch (fn [_]
|
||||
nil)))
|
||||
(js/Promise.resolve nil))))
|
||||
|
||||
(defn- <dispatch-and-schedule!
|
||||
[^js env user-id step params orchestrated]
|
||||
(-> (<dispatch-sessions-with-step! env
|
||||
user-id
|
||||
(:dispatch-sessions orchestrated)
|
||||
step
|
||||
"execution-dispatch")
|
||||
(.then (fn [dispatch-results]
|
||||
(let [orchestrated (cond-> orchestrated
|
||||
(some? dispatch-results) (assoc :dispatch-results dispatch-results))
|
||||
orchestrated (with-dispatched-task-sessions orchestrated)]
|
||||
(-> (<persist-planning-session! env params orchestrated)
|
||||
(.then (fn [_]
|
||||
(if-let [action (when step
|
||||
(replan-action orchestrated))]
|
||||
(let [delay-sec (:delay-sec action)]
|
||||
(-> (<sleep-for-replan! step delay-sec)
|
||||
(.then (fn [_]
|
||||
(let [next-params (assoc params :existing-tasks (:reconciled-tasks orchestrated))
|
||||
replan-orchestrated (orchestrate next-params)]
|
||||
(-> (<dispatch-and-schedule! env
|
||||
user-id
|
||||
step
|
||||
next-params
|
||||
replan-orchestrated)
|
||||
(.then (fn [result]
|
||||
(assoc result
|
||||
:scheduler {:executed true
|
||||
:action action
|
||||
:base-status (:status orchestrated)})))))))))
|
||||
(-> (<wait-for-replan-event! step)
|
||||
(.then (fn [replan-params]
|
||||
(if-not (map? replan-params)
|
||||
orchestrated
|
||||
(let [next-params (merge params
|
||||
replan-params
|
||||
{:existing-tasks (:reconciled-tasks orchestrated)})
|
||||
replan-orchestrated (orchestrate next-params)]
|
||||
(<dispatch-and-schedule! env
|
||||
user-id
|
||||
step
|
||||
next-params
|
||||
replan-orchestrated)))))))))))))))
|
||||
|
||||
(defn run
|
||||
[this event step]
|
||||
(let [params (if (map? (:params event))
|
||||
(merge (:params event)
|
||||
(dissoc event :params))
|
||||
event)
|
||||
env (some-> this .-env)
|
||||
user-id (some-> (:user-id params) non-empty-str)
|
||||
orchestrated (orchestrate params)]
|
||||
(-> (if (= "waiting-approval" (:status orchestrated))
|
||||
(-> (<persist-planning-session! env params orchestrated)
|
||||
(.then (fn [_]
|
||||
(<wait-for-approval-event! step)))
|
||||
(.then (fn [approval-status]
|
||||
(let [approval-status (if (contains? #{"approved" "pending" "rejected"} approval-status)
|
||||
approval-status
|
||||
"pending")
|
||||
approved-params (assoc params :approval {:decision approval-status})
|
||||
next-orchestrated (orchestrate approved-params)]
|
||||
(if (= "approved" approval-status)
|
||||
(<dispatch-and-schedule! env user-id step approved-params next-orchestrated)
|
||||
(-> (<persist-planning-session! env approved-params next-orchestrated)
|
||||
(.then (fn [_]
|
||||
next-orchestrated))))))))
|
||||
(<dispatch-and-schedule! env user-id step params orchestrated))
|
||||
(.catch (fn [error]
|
||||
(log/error :agent/planning-workflow-run-failed error)
|
||||
(-> (<persist-planning-session! env
|
||||
params
|
||||
{:status "failed"
|
||||
:planning-session-id (or (:planning-session-id orchestrated)
|
||||
(:planning-session-id params))
|
||||
:plan (:plan orchestrated)
|
||||
:reconciled-tasks (:reconciled-tasks orchestrated)
|
||||
:dispatch-sessions []
|
||||
:scheduled-actions []
|
||||
:planning-state {:approval-status (get-in orchestrated [:planning-state :approval-status])}})
|
||||
(.then (fn [_]
|
||||
{:ok false
|
||||
:status "failed"
|
||||
:error (str error)}))))))))
|
||||
84
deps/workers/src/logseq/agents/request.cljs
vendored
84
deps/workers/src/logseq/agents/request.cljs
vendored
@@ -22,29 +22,6 @@
|
||||
parsed
|
||||
default-value)))
|
||||
|
||||
(defn- non-negative-int
|
||||
[value default-value]
|
||||
(max 0 (normalize-int value default-value)))
|
||||
|
||||
(defn- normalize-bool
|
||||
[value default-value]
|
||||
(if (boolean? value)
|
||||
value
|
||||
default-value))
|
||||
|
||||
(defn- normalize-approval
|
||||
[value]
|
||||
(when (map? value)
|
||||
(let [decision (some-> (:decision value)
|
||||
non-empty-str
|
||||
string/lower-case)
|
||||
decision (if (contains? #{"pending" "approved" "rejected"} decision)
|
||||
decision
|
||||
"pending")
|
||||
approval-comment (some-> (:comment value) non-empty-str)]
|
||||
(cond-> {:decision decision}
|
||||
(string? approval-comment) (assoc :comment approval-comment)))))
|
||||
|
||||
(defn normalize-session-create
|
||||
[body]
|
||||
(when (map? body)
|
||||
@@ -71,67 +48,6 @@
|
||||
(string? runtime-provider) (assoc :runtime-provider runtime-provider)
|
||||
(string? runner-id) (assoc :runner-id runner-id)))))
|
||||
|
||||
(defn normalize-planning-create
|
||||
[body]
|
||||
(when-let [task (normalize-session-create body)]
|
||||
(let [agent (:agent task)]
|
||||
(assoc task
|
||||
:agent
|
||||
(cond
|
||||
(string? agent)
|
||||
{:provider agent
|
||||
:permission-mode "read-only"}
|
||||
|
||||
(map? agent)
|
||||
(cond-> agent
|
||||
(and (= "codex" (some-> (:provider agent) non-empty-str string/lower-case))
|
||||
(nil? (:permission-mode agent))
|
||||
(nil? (:permissionMode agent)))
|
||||
(assoc :permission-mode "read-only"))
|
||||
|
||||
:else agent)))))
|
||||
|
||||
(defn normalize-planning-workflow-create
|
||||
[body]
|
||||
(when (map? body)
|
||||
(let [workflow-id (some-> (:workflow-id body) non-empty-str)
|
||||
planning-session-id (some-> (:planning-session-id body) non-empty-str)
|
||||
user-id (some-> (:user-id body) non-empty-str)
|
||||
goal (:goal body)
|
||||
tasks (or (:tasks body) (:planned-tasks body))
|
||||
tasks (when (sequential? tasks) (vec tasks))
|
||||
planning-messages (when (sequential? (:planning-messages body))
|
||||
(vec (:planning-messages body)))
|
||||
require-approval? (true? (:require-approval body))
|
||||
auto-dispatch? (normalize-bool (:auto-dispatch body) true)
|
||||
auto-replan? (normalize-bool (:auto-replan body) false)
|
||||
replan-delay-sec (non-negative-int (:replan-delay-sec body) 0)
|
||||
replan-note (or (some-> (:replan-note body) non-empty-str)
|
||||
(some-> (:comment body) non-empty-str))
|
||||
approval (normalize-approval (:approval body))
|
||||
project (when (map? (:project body)) (:project body))
|
||||
agent (:agent body)
|
||||
runtime-provider (some-> (:runtime-provider body)
|
||||
non-empty-str
|
||||
string/lower-case)
|
||||
runner-id (some-> (:runner-id body) non-empty-str)]
|
||||
(cond-> {:require-approval require-approval?
|
||||
:auto-dispatch auto-dispatch?
|
||||
:auto-replan auto-replan?
|
||||
:replan-delay-sec replan-delay-sec}
|
||||
(string? workflow-id) (assoc :workflow-id workflow-id)
|
||||
(string? planning-session-id) (assoc :planning-session-id planning-session-id)
|
||||
(string? user-id) (assoc :user-id user-id)
|
||||
(map? goal) (assoc :goal goal)
|
||||
(sequential? tasks) (assoc :tasks tasks)
|
||||
(sequential? planning-messages) (assoc :planning-messages planning-messages)
|
||||
(map? project) (assoc :project project)
|
||||
(some? agent) (assoc :agent agent)
|
||||
(string? runtime-provider) (assoc :runtime-provider runtime-provider)
|
||||
(string? runner-id) (assoc :runner-id runner-id)
|
||||
(string? replan-note) (assoc :replan-note replan-note)
|
||||
(map? approval) (assoc :approval approval)))))
|
||||
|
||||
(defn normalize-runner-register
|
||||
[body user-id]
|
||||
(when (map? body)
|
||||
|
||||
10
deps/workers/src/logseq/agents/routes.cljs
vendored
10
deps/workers/src/logseq/agents/routes.cljs
vendored
@@ -5,16 +5,6 @@
|
||||
[["/auth"
|
||||
["/chatgpt/import" {:methods {"POST" :auth.chatgpt/import}}]
|
||||
["/chatgpt/status" {:methods {"GET" :auth.chatgpt/status}}]]
|
||||
["/planning"
|
||||
["/sessions" {:methods {"POST" :planning.sessions/create}}]
|
||||
["/sessions/:planning-session-id" {:methods {"GET" :planning.sessions/get}}]
|
||||
["/sessions/:planning-session-id/approval" {:methods {"POST" :planning.sessions/approval}}]
|
||||
["/sessions/:planning-session-id/tasks/sync" {:methods {"POST" :planning.sessions/tasks.sync}}]
|
||||
["/sessions/:planning-session-id/replan" {:methods {"POST" :planning.sessions/replan}}]
|
||||
["/chat/:planning-session-id" {:methods {"GET" :planning.chat/transport
|
||||
"POST" :planning.chat/transport}}]
|
||||
["/workflows" {:methods {"POST" :planning.workflows/create}}]
|
||||
["/workflows/:workflow-id" {:methods {"GET" :planning.workflows/get}}]]
|
||||
["/sessions"
|
||||
["" {:methods {"POST" :sessions/create}}]
|
||||
["/:session-id"
|
||||
|
||||
109
deps/workers/src/logseq/agents/worker.cljs
vendored
109
deps/workers/src/logseq/agents/worker.cljs
vendored
@@ -1,15 +1,11 @@
|
||||
(ns logseq.agents.worker
|
||||
;; Turn off false defclass errors
|
||||
{:clj-kondo/config {:linters {:unresolved-symbol {:level :off}}}}
|
||||
(:require ["agents" :refer [Agent]]
|
||||
["cloudflare:workers" :refer [DurableObject WorkflowEntrypoint]]
|
||||
(:require ["cloudflare:workers" :refer [DurableObject]]
|
||||
[logseq.agents.dispatch :as dispatch]
|
||||
[logseq.agents.do :as agent-do]
|
||||
[logseq.agents.planning-agent :as planning-agent]
|
||||
[logseq.agents.planning-workflow :as planning-workflow]
|
||||
[logseq.sync.logging :as logging]
|
||||
[logseq.sync.sentry.worker :as sentry]
|
||||
[promesa.core :as p]
|
||||
[shadow.cljs.modern :refer (defclass)]))
|
||||
|
||||
(logging/install!)
|
||||
@@ -32,106 +28,3 @@
|
||||
Object
|
||||
(fetch [this request]
|
||||
(agent-do/handle-fetch this request)))
|
||||
|
||||
(defclass PlanningWorkflow
|
||||
(extends WorkflowEntrypoint)
|
||||
|
||||
(constructor [this ctx env]
|
||||
(super ctx env)
|
||||
(set! (.-ctx this) ctx)
|
||||
(set! (.-env this) env))
|
||||
|
||||
Object
|
||||
(run [this event step]
|
||||
(planning-workflow/run this event step)))
|
||||
|
||||
(defn- planning-session-id
|
||||
[this props]
|
||||
(or (some-> props :planning-session-id str)
|
||||
(some-> this .-name str)
|
||||
"default"))
|
||||
|
||||
(defn- get-planning-state
|
||||
[this props]
|
||||
(let [planning-session-id (planning-session-id this props)
|
||||
current-state (some-> this .-state (js->clj :keywordize-keys true))]
|
||||
(planning-agent/normalize-state planning-session-id current-state)))
|
||||
|
||||
(defn- set-planning-state!
|
||||
[^js this state]
|
||||
(let [state' (clj->js state)
|
||||
set-state (aget this "setState")
|
||||
set-state-internal (aget this "_setStateInternal")]
|
||||
(cond
|
||||
(fn? set-state)
|
||||
(.call set-state this state')
|
||||
|
||||
(fn? set-state-internal)
|
||||
(.call set-state-internal this state' "server")
|
||||
|
||||
:else
|
||||
(set! (.-_state this) state')))
|
||||
state)
|
||||
|
||||
(defclass PlanningSessionAgent
|
||||
(extends Agent)
|
||||
|
||||
(constructor [this ctx env]
|
||||
(super ctx env)
|
||||
(set! (.-ctx this) ctx)
|
||||
(set! (.-env this) env))
|
||||
|
||||
Object
|
||||
(onStart [this props]
|
||||
(let [props (when (some? props)
|
||||
(js->clj props :keywordize-keys true))]
|
||||
(set-planning-state! this (get-planning-state this props))))
|
||||
|
||||
(onRequest [this request]
|
||||
(let [method (.-method request)]
|
||||
(cond
|
||||
(= method "GET")
|
||||
(planning-agent/json-response (get-planning-state this nil) 200)
|
||||
|
||||
(= method "POST")
|
||||
(-> (.text request)
|
||||
(p/then (fn [raw]
|
||||
(if-let [content (planning-agent/parse-message-content raw)]
|
||||
(let [session-id (planning-session-id this nil)
|
||||
next-state (-> (get-planning-state this nil)
|
||||
(planning-agent/append-message session-id "user" content)
|
||||
(set-planning-state! this))]
|
||||
(planning-agent/json-response {:ok true
|
||||
:state next-state}
|
||||
200))
|
||||
(planning-agent/json-response {:error "invalid message"} 400))))
|
||||
(p/catch (fn [_error]
|
||||
(planning-agent/json-response {:error "invalid message"} 400))))
|
||||
|
||||
:else
|
||||
(planning-agent/json-response {:error "method not allowed"} 405))))
|
||||
|
||||
(onConnect [this connection _ctx]
|
||||
(let [state (get-planning-state this nil)]
|
||||
(.send connection
|
||||
(js/JSON.stringify
|
||||
(clj->js {:type "planning.state"
|
||||
:state state})))))
|
||||
|
||||
(onMessage [this connection message]
|
||||
(let [content (planning-agent/parse-message-content message)]
|
||||
(if-not (string? content)
|
||||
(.send connection
|
||||
(js/JSON.stringify
|
||||
#js {:type "planning.error"
|
||||
:message "invalid message"}))
|
||||
(let [session-id (planning-session-id this nil)
|
||||
next-state (-> (get-planning-state this nil)
|
||||
(planning-agent/append-message session-id "user" content)
|
||||
(set-planning-state! this))
|
||||
latest-message (last (:messages next-state))]
|
||||
(.broadcast this
|
||||
(js/JSON.stringify
|
||||
(clj->js {:type "planning.message"
|
||||
:message latest-message
|
||||
:state next-state}))))))))
|
||||
|
||||
30
deps/workers/src/logseq/sync/malli_schema.cljs
vendored
30
deps/workers/src/logseq/sync/malli_schema.cljs
vendored
@@ -373,32 +373,6 @@
|
||||
[:snapshot-id {:optional true} [:maybe :string]]
|
||||
[:message {:optional true} [:maybe :string]]])
|
||||
|
||||
(def planning-session-response-schema
|
||||
[:map
|
||||
[:planning-session-id :string]
|
||||
[:status :string]
|
||||
[:workflow-id {:optional true} [:maybe :string]]
|
||||
[:chat-path {:optional true} :string]
|
||||
[:goal {:optional true} :map]
|
||||
[:project {:optional true} :map]
|
||||
[:agent {:optional true} :any]
|
||||
[:approval-status {:optional true} :string]
|
||||
[:require-approval {:optional true} :boolean]
|
||||
[:auto-dispatch {:optional true} :boolean]
|
||||
[:auto-replan {:optional true} :boolean]
|
||||
[:replan-delay-sec {:optional true} :int]
|
||||
[:plan {:optional true} :map]
|
||||
[:scheduled-actions {:optional true} [:sequential :map]]
|
||||
[:dispatch-sessions {:optional true} [:sequential :map]]
|
||||
[:created-at {:optional true} :int]
|
||||
[:updated-at {:optional true} :int]])
|
||||
|
||||
(def planning-workflow-response-schema
|
||||
[:map
|
||||
[:workflow-id :string]
|
||||
[:status :string]
|
||||
[:planning-session-id {:optional true} :string]])
|
||||
|
||||
(def runner-response-schema
|
||||
[:map
|
||||
[:runner-id :string]
|
||||
@@ -477,10 +451,6 @@
|
||||
:sessions/snapshot sessions-snapshot-response-schema
|
||||
:sessions/events sessions-events-response-schema
|
||||
:sessions/branches sessions-branches-response-schema
|
||||
:planning.sessions/create planning-session-response-schema
|
||||
:planning.sessions/get planning-session-response-schema
|
||||
:planning.workflows/create planning-workflow-response-schema
|
||||
:planning.workflows/get planning-workflow-response-schema
|
||||
:auth.chatgpt/import auth-chatgpt-status-response-schema
|
||||
:auth.chatgpt/status auth-chatgpt-status-response-schema
|
||||
:runners/register runners-register-response-schema
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
(index-handler/handle-fetch #js {:env env :d1 (aget env "DB")} request)
|
||||
|
||||
(or (string/starts-with? path "/auth")
|
||||
(string/starts-with? path "/planning")
|
||||
(string/starts-with? path "/sessions"))
|
||||
(if-let [^js agents-service (aget env "AGENTS_SERVICE")]
|
||||
(if (local-dev-host? request)
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
(ns logseq.agents.handler-test
|
||||
(:require [cljs.test :refer [async deftest is]]
|
||||
[logseq.agents.handler :as handler]
|
||||
[logseq.sync.platform.core :as platform]))
|
||||
|
||||
(deftest planning-session-get-requires-planning-store-test
|
||||
(async done
|
||||
(let [request (platform/request "http://example.com/planning/sessions/plan-1"
|
||||
#js {:method "GET"})]
|
||||
(-> (js/Promise.resolve
|
||||
(handler/handle {:env #js {}
|
||||
:request request
|
||||
:url (platform/request-url request)
|
||||
:claims #js {"sub" "user-1"}
|
||||
:route {:handler :planning.sessions/get
|
||||
:path-params {:planning-session-id "plan-1"}}}))
|
||||
(.then (fn [response]
|
||||
(is (= 503 (.-status response)))
|
||||
(done)))
|
||||
(.catch (fn [error]
|
||||
(is false (str "unexpected error: " error))
|
||||
(done)))))))
|
||||
|
||||
(deftest planning-chat-transport-requires-agent-binding-test
|
||||
(async done
|
||||
(let [request (platform/request "http://example.com/planning/chat/plan-1"
|
||||
#js {:method "GET"})]
|
||||
(-> (js/Promise.resolve
|
||||
(handler/handle {:env #js {}
|
||||
:request request
|
||||
:url (platform/request-url request)
|
||||
:claims #js {"sub" "user-1"}
|
||||
:route {:handler :planning.chat/transport
|
||||
:path-params {:planning-session-id "plan-1"}}}))
|
||||
(.then (fn [response]
|
||||
(is (= 503 (.-status response)))
|
||||
(done)))
|
||||
(.catch (fn [error]
|
||||
(is false (str "unexpected error: " error))
|
||||
(done)))))))
|
||||
|
||||
(deftest planning-session-replan-requires-workflow-binding-test
|
||||
(async done
|
||||
(let [request (platform/request "http://example.com/planning/sessions/plan-1/replan"
|
||||
#js {:method "POST"
|
||||
:headers #js {"content-type" "application/json"}
|
||||
:body (js/JSON.stringify #js {:tasks #js []})})]
|
||||
(-> (js/Promise.resolve
|
||||
(handler/handle {:env #js {}
|
||||
:request request
|
||||
:url (platform/request-url request)
|
||||
:claims #js {"sub" "user-1"}
|
||||
:route {:handler :planning.sessions/replan
|
||||
:path-params {:planning-session-id "plan-1"}}}))
|
||||
(.then (fn [response]
|
||||
(is (= 503 (.-status response)))
|
||||
(done)))
|
||||
(.catch (fn [error]
|
||||
(is false (str "unexpected error: " error))
|
||||
(done)))))))
|
||||
@@ -1,122 +0,0 @@
|
||||
(ns logseq.agents.planning-workflow-test
|
||||
(:require [cljs.test :refer [async deftest is]]
|
||||
[logseq.agents.planning-workflow :as planning-workflow]))
|
||||
|
||||
(deftest build-plan-test
|
||||
(let [plan (planning-workflow/build-plan
|
||||
{:goal {:title "Plan Goal"
|
||||
:description "Build a planner"}
|
||||
:tasks [{:title "Task A"
|
||||
:description "Desc A"}
|
||||
{:content "Explicit content"}]})]
|
||||
(is (= "Build a planner" (:goal-understanding plan)))
|
||||
(is (= [{:title "Task A"
|
||||
:content "Task A\nDesc A"
|
||||
:description "Desc A"}
|
||||
{:title "Task 2"
|
||||
:content "Explicit content"}]
|
||||
(:tasks plan)))))
|
||||
|
||||
(deftest run-awaits-approval-before-dispatch-test
|
||||
(async done
|
||||
(-> (planning-workflow/run nil
|
||||
{:planning-session-id "plan-1"
|
||||
:goal {:title "Plan Goal"
|
||||
:description "Build planning workflow"}
|
||||
:tasks [{:title "Task A"
|
||||
:description "Desc A"}]
|
||||
:project {:id "project-1"
|
||||
:repo-url "https://github.com/example/repo"
|
||||
:base-branch "main"}
|
||||
:agent {:provider "codex"}
|
||||
:runtime-provider "local-runner"
|
||||
:runner-id "runner-1"
|
||||
:require-approval true
|
||||
:approval {:decision "pending"}
|
||||
:auto-dispatch true
|
||||
:auto-replan true
|
||||
:replan-delay-sec 300}
|
||||
nil)
|
||||
(.then (fn [result]
|
||||
(is (= "waiting-approval" (:status result)))
|
||||
(is (= [] (:dispatch-sessions result)))
|
||||
(is (= "pending" (get-in result [:planning-state :approval-status])))
|
||||
(is (= "https://github.com/example/repo"
|
||||
(get-in result [:planning-state :repo-aware :repo-url])))
|
||||
(is (= [] (:scheduled-actions result)))
|
||||
(done)))
|
||||
(.catch (fn [error]
|
||||
(is false (str "unexpected error: " error))
|
||||
(done))))))
|
||||
|
||||
(deftest run-dispatches-approved-repo-aware-tasks-and-schedules-replan-test
|
||||
(async done
|
||||
(-> (planning-workflow/run nil
|
||||
{:planning-session-id "plan-2"
|
||||
:goal {:title "Plan Goal"
|
||||
:description "Build planning workflow"}
|
||||
:tasks [{:title "Task A"
|
||||
:description "Desc A"}]
|
||||
:project {:id "project-1"
|
||||
:repo-url "https://github.com/example/repo"
|
||||
:base-branch "main"}
|
||||
:agent {:provider "codex"
|
||||
:mode "gpt-5-codex"}
|
||||
:runtime-provider "local-runner"
|
||||
:runner-id "runner-1"
|
||||
:require-approval true
|
||||
:approval {:decision "approved"}
|
||||
:auto-dispatch true
|
||||
:auto-replan true
|
||||
:replan-delay-sec 120}
|
||||
nil)
|
||||
(.then (fn [result]
|
||||
(is (= "dispatching" (:status result)))
|
||||
(is (= 1 (count (:dispatch-sessions result))))
|
||||
(is (= "https://github.com/example/repo"
|
||||
(get-in result [:dispatch-sessions 0 :project :repo-url])))
|
||||
(is (= "codex"
|
||||
(get-in result [:dispatch-sessions 0 :agent :provider])))
|
||||
(is (= "local-runner" (get-in result [:dispatch-sessions 0 :runtime-provider])))
|
||||
(is (= "runner-1" (get-in result [:dispatch-sessions 0 :runner-id])))
|
||||
(is (= [{:type "replan"
|
||||
:delay-sec 120}]
|
||||
(:scheduled-actions result)))
|
||||
(done)))
|
||||
(.catch (fn [error]
|
||||
(is false (str "unexpected error: " error))
|
||||
(done))))))
|
||||
|
||||
(deftest run-replanning-preserves-execution-owned-fields-test
|
||||
(async done
|
||||
(-> (planning-workflow/run nil
|
||||
{:planning-session-id "plan-3"
|
||||
:goal {:title "Plan Goal"}
|
||||
:tasks [{:task-uuid "task-1"
|
||||
:title "Task A (replanned)"
|
||||
:description "Desc A"}
|
||||
{:task-uuid "task-2"
|
||||
:title "Task B"
|
||||
:description "Desc B"}]
|
||||
:existing-tasks [{:task-uuid "task-1"
|
||||
:title "Task A"
|
||||
:status "Doing"
|
||||
:session-id "sess-1"
|
||||
:pr-url "https://github.com/example/repo/pull/1"}]
|
||||
:project {:id "project-1"
|
||||
:repo-url "https://github.com/example/repo"}
|
||||
:agent {:provider "codex"}
|
||||
:approval {:decision "approved"}
|
||||
:auto-dispatch false
|
||||
:auto-replan false}
|
||||
nil)
|
||||
(.then (fn [result]
|
||||
(is (= "Task A" (get-in result [:reconciled-tasks 0 :title])))
|
||||
(is (= "sess-1" (get-in result [:reconciled-tasks 0 :session-id])))
|
||||
(is (= "https://github.com/example/repo/pull/1"
|
||||
(get-in result [:reconciled-tasks 0 :pr-url])))
|
||||
(is (= 2 (count (:reconciled-tasks result))))
|
||||
(done)))
|
||||
(.catch (fn [error]
|
||||
(is false (str "unexpected error: " error))
|
||||
(done))))))
|
||||
@@ -63,95 +63,6 @@
|
||||
:pr-enabled true}}
|
||||
normalized)))))
|
||||
|
||||
(deftest normalize-planning-create-test
|
||||
(testing "normalizes planning request into agent task with codex read-only default"
|
||||
(let [body {:session-id "plan-1"
|
||||
:node-id "goal-1"
|
||||
:node-title "Plan Goal"
|
||||
:content "Plan this task graph"
|
||||
:project {:id "project-1"
|
||||
:title "Demo Project"
|
||||
:repo-url "https://github.com/example/repo"}
|
||||
:agent "codex"}
|
||||
normalized (request/normalize-planning-create body)]
|
||||
(is (= {:id "plan-1"
|
||||
:source {:node-id "goal-1"
|
||||
:node-title "Plan Goal"}
|
||||
:intent {:content "Plan this task graph"}
|
||||
:project {:id "project-1"
|
||||
:title "Demo Project"
|
||||
:repo-url "https://github.com/example/repo"}
|
||||
:agent {:provider "codex"
|
||||
:permission-mode "read-only"}
|
||||
:capabilities {:push-enabled true
|
||||
:pr-enabled true}}
|
||||
normalized))))
|
||||
|
||||
(testing "keeps explicit agent permission mode"
|
||||
(let [body {:session-id "plan-2"
|
||||
:node-id "goal-2"
|
||||
:node-title "Plan Goal"
|
||||
:content "Plan this task graph"
|
||||
:project {:id "project-1"
|
||||
:title "Demo Project"
|
||||
:repo-url "https://github.com/example/repo"}
|
||||
:agent {:provider "codex"
|
||||
:permission-mode "full-access"}}
|
||||
normalized (request/normalize-planning-create body)]
|
||||
(is (= "full-access" (get-in normalized [:agent :permission-mode]))))))
|
||||
|
||||
(deftest normalize-planning-workflow-create-test
|
||||
(let [body {:workflow-id "wf-1"
|
||||
:planning-session-id "plan-1"
|
||||
:user-id "user-1"
|
||||
:goal {:title "Plan Goal"
|
||||
:description "Plan this feature"}
|
||||
:tasks [{:title "Task A"
|
||||
:description "Desc A"}]
|
||||
:project {:id "project-1"
|
||||
:repo-url "https://github.com/example/repo"
|
||||
:base-branch "main"}
|
||||
:agent {:provider "codex"}
|
||||
:runtime-provider "local-runner"
|
||||
:runner-id "runner-1"
|
||||
:approval {:decision "approved"
|
||||
:comment "looks good"}
|
||||
:auto-dispatch true
|
||||
:auto-replan true
|
||||
:replan-delay-sec 300
|
||||
:require-approval true}
|
||||
normalized (request/normalize-planning-workflow-create body)]
|
||||
(is (= {:workflow-id "wf-1"
|
||||
:planning-session-id "plan-1"
|
||||
:user-id "user-1"
|
||||
:goal {:title "Plan Goal"
|
||||
:description "Plan this feature"}
|
||||
:tasks [{:title "Task A"
|
||||
:description "Desc A"}]
|
||||
:project {:id "project-1"
|
||||
:repo-url "https://github.com/example/repo"
|
||||
:base-branch "main"}
|
||||
:agent {:provider "codex"}
|
||||
:runtime-provider "local-runner"
|
||||
:runner-id "runner-1"
|
||||
:approval {:decision "approved"
|
||||
:comment "looks good"}
|
||||
:auto-dispatch true
|
||||
:auto-replan true
|
||||
:replan-delay-sec 300
|
||||
:require-approval true}
|
||||
normalized))))
|
||||
|
||||
(deftest normalize-planning-workflow-create-defaults-test
|
||||
(let [body {:goal {:title "Plan Goal"}}
|
||||
normalized (request/normalize-planning-workflow-create body)]
|
||||
(is (= {:goal {:title "Plan Goal"}
|
||||
:require-approval false
|
||||
:auto-dispatch true
|
||||
:auto-replan false
|
||||
:replan-delay-sec 0}
|
||||
normalized))))
|
||||
|
||||
(deftest sessions-pr-coerce-test
|
||||
(testing "accepts sessions/pr request payload"
|
||||
(let [body {:title "feat: add m14 publish"
|
||||
|
||||
22
deps/workers/test/logseq/agents/routes_test.cljs
vendored
22
deps/workers/test/logseq/agents/routes_test.cljs
vendored
@@ -68,25 +68,3 @@
|
||||
(is (= :auth.chatgpt/import (:handler match))))
|
||||
(let [match (routes/match-route "GET" "/auth/chatgpt/status")]
|
||||
(is (= :auth.chatgpt/status (:handler match))))))
|
||||
|
||||
(deftest match-route-planning-test
|
||||
(testing "planning routes"
|
||||
(let [match (routes/match-route "POST" "/planning/sessions")]
|
||||
(is (= :planning.sessions/create (:handler match))))
|
||||
(let [match (routes/match-route "GET" "/planning/sessions/plan-1")]
|
||||
(is (= :planning.sessions/get (:handler match)))
|
||||
(is (= "plan-1" (get-in match [:path-params :planning-session-id]))))
|
||||
(let [match (routes/match-route "POST" "/planning/sessions/plan-1/approval")]
|
||||
(is (= :planning.sessions/approval (:handler match)))
|
||||
(is (= "plan-1" (get-in match [:path-params :planning-session-id]))))
|
||||
(let [match (routes/match-route "POST" "/planning/sessions/plan-1/replan")]
|
||||
(is (= :planning.sessions/replan (:handler match)))
|
||||
(is (= "plan-1" (get-in match [:path-params :planning-session-id]))))
|
||||
(let [match (routes/match-route "GET" "/planning/chat/plan-1")]
|
||||
(is (= :planning.chat/transport (:handler match)))
|
||||
(is (= "plan-1" (get-in match [:path-params :planning-session-id]))))
|
||||
(let [match (routes/match-route "POST" "/planning/workflows")]
|
||||
(is (= :planning.workflows/create (:handler match))))
|
||||
(let [match (routes/match-route "GET" "/planning/workflows/workflow-1")]
|
||||
(is (= :planning.workflows/get (:handler match)))
|
||||
(is (= "workflow-1" (get-in match [:path-params :workflow-id]))))))
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
(ns logseq.agents.runtime-transport-test-runner
|
||||
(:require [cljs.test :as ct]
|
||||
[logseq.agents.handler-test]
|
||||
[logseq.agents.planning-workflow-test]
|
||||
[logseq.agents.request-test]
|
||||
[logseq.agents.routes-test]
|
||||
[logseq.agents.runtime-provider-test]
|
||||
[logseq.agents.sandbox-test]
|
||||
[shadow.test :as st]
|
||||
@@ -23,17 +19,6 @@
|
||||
(defn main [& _args]
|
||||
(reset-test-data!)
|
||||
(ct/test-vars [#'logseq.agents.runtime-provider-test/local-runner-provider-provision-test
|
||||
#'logseq.agents.planning-workflow-test/build-plan-test
|
||||
#'logseq.agents.planning-workflow-test/run-awaits-approval-before-dispatch-test
|
||||
#'logseq.agents.planning-workflow-test/run-dispatches-approved-repo-aware-tasks-and-schedules-replan-test
|
||||
#'logseq.agents.planning-workflow-test/run-replanning-preserves-execution-owned-fields-test
|
||||
#'logseq.agents.routes-test/match-route-planning-test
|
||||
#'logseq.agents.request-test/normalize-planning-create-test
|
||||
#'logseq.agents.request-test/normalize-planning-workflow-create-test
|
||||
#'logseq.agents.request-test/normalize-planning-workflow-create-defaults-test
|
||||
#'logseq.agents.handler-test/planning-session-get-requires-planning-store-test
|
||||
#'logseq.agents.handler-test/planning-chat-transport-requires-agent-binding-test
|
||||
#'logseq.agents.handler-test/planning-session-replan-requires-workflow-binding-test
|
||||
#'logseq.agents.sandbox-test/session-endpoint-test
|
||||
#'logseq.agents.sandbox-test/create-session-payload-test
|
||||
#'logseq.agents.sandbox-test/send-message-uses-acp-prompt-test
|
||||
|
||||
38
deps/workers/test/logseq/sync/worker_test.cljs
vendored
38
deps/workers/test/logseq/sync/worker_test.cljs
vendored
@@ -1,11 +1,11 @@
|
||||
(ns logseq.sync.worker-test
|
||||
(:require [cljs.test :refer [async deftest is]]
|
||||
[datascript.core :as d]
|
||||
[logseq.db.common.order :as db-order]
|
||||
[logseq.db.frontend.schema :as db-schema]
|
||||
[logseq.sync.order :as sync-order]
|
||||
[logseq.sync.platform.core :as platform]
|
||||
[logseq.sync.worker.dispatch :as dispatch]))
|
||||
[logseq.sync.worker.dispatch :as dispatch]
|
||||
[logseq.db.common.order :as db-order]
|
||||
[logseq.db.frontend.schema :as db-schema]))
|
||||
|
||||
(defn- new-conn []
|
||||
(d/create-conn db-schema/schema))
|
||||
@@ -55,17 +55,6 @@
|
||||
(is false (str "unexpected error: " error))
|
||||
(done)))))))
|
||||
|
||||
(deftest dispatch-worker-fetch-planning-service-unavailable-test
|
||||
(async done
|
||||
(let [request (platform/request "http://example.com/planning/sessions" #js {:method "POST"})
|
||||
resp (dispatch/handle-worker-fetch request #js {})]
|
||||
(-> (.then resp (fn [resolved]
|
||||
(is (= 503 (.-status resolved)))
|
||||
(done)))
|
||||
(.catch (fn [error]
|
||||
(is false (str "unexpected error: " error))
|
||||
(done)))))))
|
||||
|
||||
(deftest dispatch-worker-fetch-sessions-forward-to-agents-service-test
|
||||
(async done
|
||||
(let [request (platform/request "http://example.com/sessions/session-2/events?since=1" #js {:method "GET"})
|
||||
@@ -85,27 +74,6 @@
|
||||
(is false (str "unexpected error: " error))
|
||||
(done)))))))
|
||||
|
||||
(deftest dispatch-worker-fetch-planning-forward-to-agents-service-test
|
||||
(async done
|
||||
(let [request (platform/request "http://example.com/planning/sessions" #js {:method "POST"
|
||||
:headers #js {"content-type" "application/json"}
|
||||
:body (js/JSON.stringify #js {:session-id "plan-1"})})
|
||||
captured-url (atom nil)
|
||||
env #js {:AGENTS_SERVICE #js {:fetch (fn [forwarded]
|
||||
(reset! captured-url (.-url forwarded))
|
||||
(js/Promise.resolve
|
||||
(js/Response. (js/JSON.stringify #js {:ok true})
|
||||
#js {:status 202
|
||||
:headers #js {"content-type" "application/json"}})))}}]
|
||||
(-> (dispatch/handle-worker-fetch request env)
|
||||
(.then (fn [resolved]
|
||||
(is (= 202 (.-status resolved)))
|
||||
(is (= "http://example.com/planning/sessions" @captured-url))
|
||||
(done)))
|
||||
(.catch (fn [error]
|
||||
(is false (str "unexpected error: " error))
|
||||
(done)))))))
|
||||
|
||||
(deftest dispatch-worker-fetch-sessions-local-retry-test
|
||||
(async done
|
||||
(let [request (platform/request "http://127.0.0.1:8787/sessions/session-3"
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
create table if not exists planning_sessions (
|
||||
planning_session_id text primary key,
|
||||
user_id text not null,
|
||||
workflow_id text,
|
||||
status text not null default 'queued',
|
||||
goal_json text,
|
||||
plan_json text,
|
||||
project_json text,
|
||||
agent_json text,
|
||||
approval_status text not null default 'pending',
|
||||
require_approval integer not null default 0,
|
||||
auto_dispatch integer not null default 1,
|
||||
auto_replan integer not null default 0,
|
||||
replan_delay_sec integer not null default 0,
|
||||
scheduled_actions_json text,
|
||||
dispatch_sessions_json text,
|
||||
last_error text,
|
||||
created_at integer not null,
|
||||
updated_at integer not null
|
||||
);
|
||||
|
||||
create index if not exists idx_planning_sessions_user_updated_at
|
||||
on planning_sessions (user_id, updated_at desc);
|
||||
|
||||
create index if not exists idx_planning_sessions_workflow_id
|
||||
on planning_sessions (workflow_id);
|
||||
39
deps/workers/worker/wrangler.agents.toml
vendored
39
deps/workers/worker/wrangler.agents.toml
vendored
@@ -16,15 +16,6 @@ invocation_logs = false
|
||||
name = "LOGSEQ_AGENT_SESSION_DO"
|
||||
class_name = "AgentSessionDO"
|
||||
|
||||
[[durable_objects.bindings]]
|
||||
name = "PLANNING_AGENT"
|
||||
class_name = "PlanningSessionAgent"
|
||||
|
||||
[[workflows]]
|
||||
binding = "PLANNING_WORKFLOW"
|
||||
name = "logseq-agents-planning-dev"
|
||||
class_name = "PlanningWorkflow"
|
||||
|
||||
[[r2_buckets]]
|
||||
binding = "BACKUP_BUCKET"
|
||||
bucket_name = "logseq-sync-assets-dev"
|
||||
@@ -39,10 +30,6 @@ database_id = "00325aa2-c805-4693-b599-900a25dcde42"
|
||||
tag = "v1"
|
||||
new_sqlite_classes = [ "AgentSessionDO" ]
|
||||
|
||||
[[migrations]]
|
||||
tag = "v2"
|
||||
new_sqlite_classes = [ "PlanningSessionAgent" ]
|
||||
|
||||
[vars]
|
||||
COGNITO_JWKS_URL = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_dtagLnju8/.well-known/jwks.json"
|
||||
COGNITO_ISSUER = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_dtagLnju8"
|
||||
@@ -69,15 +56,6 @@ SENTRY_TRACES_SAMPLE_RATE = "0.1"
|
||||
name = "LOGSEQ_AGENT_SESSION_DO"
|
||||
class_name = "AgentSessionDO"
|
||||
|
||||
[[env.staging.durable_objects.bindings]]
|
||||
name = "PLANNING_AGENT"
|
||||
class_name = "PlanningSessionAgent"
|
||||
|
||||
[[env.staging.workflows]]
|
||||
binding = "PLANNING_WORKFLOW"
|
||||
name = "logseq-agents-planning-staging"
|
||||
class_name = "PlanningWorkflow"
|
||||
|
||||
[[env.staging.r2_buckets]]
|
||||
binding = "BACKUP_BUCKET"
|
||||
bucket_name = "logseq-sync-assets-dev"
|
||||
@@ -92,10 +70,6 @@ database_id = "00325aa2-c805-4693-b599-900a25dcde42"
|
||||
tag = "v1"
|
||||
new_sqlite_classes = [ "AgentSessionDO" ]
|
||||
|
||||
[[env.staging.migrations]]
|
||||
tag = "v2"
|
||||
new_sqlite_classes = [ "PlanningSessionAgent" ]
|
||||
|
||||
[env.staging.version_metadata]
|
||||
binding = "CF_VERSION_METADATA"
|
||||
|
||||
@@ -117,15 +91,6 @@ SENTRY_TRACES_SAMPLE_RATE = "0.1"
|
||||
name = "LOGSEQ_AGENT_SESSION_DO"
|
||||
class_name = "AgentSessionDO"
|
||||
|
||||
[[env.prod.durable_objects.bindings]]
|
||||
name = "PLANNING_AGENT"
|
||||
class_name = "PlanningSessionAgent"
|
||||
|
||||
[[env.prod.workflows]]
|
||||
binding = "PLANNING_WORKFLOW"
|
||||
name = "logseq-agents-planning-prod"
|
||||
class_name = "PlanningWorkflow"
|
||||
|
||||
[[env.prod.r2_buckets]]
|
||||
binding = "BACKUP_BUCKET"
|
||||
bucket_name = "logseq-sync-assets-prod"
|
||||
@@ -140,9 +105,5 @@ database_id = "4c80e058-69b5-4985-88d1-f53711d817ba"
|
||||
tag = "v1"
|
||||
new_sqlite_classes = [ "AgentSessionDO" ]
|
||||
|
||||
[[env.prod.migrations]]
|
||||
tag = "v2"
|
||||
new_sqlite_classes = [ "PlanningSessionAgent" ]
|
||||
|
||||
[env.prod.version_metadata]
|
||||
binding = "CF_VERSION_METADATA"
|
||||
|
||||
862
deps/workers/yarn.lock
vendored
862
deps/workers/yarn.lock
vendored
File diff suppressed because it is too large
Load Diff
@@ -149,57 +149,6 @@ Errors and idempotency:
|
||||
- GET /sandbox/sessions/:id/stream
|
||||
- server-sent events of normalized agent events
|
||||
|
||||
## Planning Layer
|
||||
- Planning sessions are separate from execution sessions.
|
||||
- Planning sessions are backed by a Cloudflare Agent instance and addressed by
|
||||
`planning-session-id`.
|
||||
- Planning workflows are backed by Cloudflare Workflows and persist their
|
||||
orchestration state into `planning_sessions`.
|
||||
- Planning state is product-visible through:
|
||||
- `GET /planning/sessions/:planning-session-id`
|
||||
- `POST /planning/sessions`
|
||||
- `POST /planning/sessions/:planning-session-id/approval`
|
||||
- `POST /planning/sessions/:planning-session-id/replan`
|
||||
- `GET|POST /planning/chat/:planning-session-id`
|
||||
|
||||
### Planning Session Shape
|
||||
- `planning-session-id`
|
||||
- `workflow-id`
|
||||
- `status`
|
||||
- `goal`
|
||||
- `project`
|
||||
- `agent`
|
||||
- `plan`
|
||||
- `approval-status`
|
||||
- `require-approval`
|
||||
- `auto-dispatch`
|
||||
- `auto-replan`
|
||||
- `replan-delay-sec`
|
||||
- `scheduled-actions`
|
||||
- `dispatch-sessions`
|
||||
|
||||
### Planning To Execution Mapping
|
||||
- A planning task becomes a normal execution `POST /sessions` payload.
|
||||
- The mapping is:
|
||||
- `dispatch-session.id` -> `sessions/create.session-id`
|
||||
- `dispatch-session.source.node-id` -> `source.node-id`
|
||||
- `dispatch-session.source.node-title` -> `source.node-title`
|
||||
- `dispatch-session.intent.content` -> `intent.content`
|
||||
- `dispatch-session.project` -> `project`
|
||||
- `dispatch-session.agent` -> `agent`
|
||||
- `dispatch-session.runtime-provider` -> `runtime-provider`
|
||||
- `dispatch-session.runner-id` -> `runner-id`
|
||||
- `dispatch-session.capabilities` -> `capabilities`
|
||||
- Execution remains on the existing `/sessions` substrate; planning never
|
||||
provisions a second execution backend.
|
||||
|
||||
### Planning Ownership Rules
|
||||
- Planning session reads/writes are always resolved against the authenticated
|
||||
`claims.sub`.
|
||||
- Planning chat transport is not forwarded by `planning-session-id` alone.
|
||||
- Approval and replan act on the stored planning session and its tracked
|
||||
workflow, not on arbitrary workflow ids supplied by the client.
|
||||
|
||||
## Example: Session Creation Payload
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -154,8 +154,8 @@
|
||||
"@tabler/icons-react": "^2.47.0",
|
||||
"@tabler/icons-webfont": "^2.47.0",
|
||||
"@tippyjs/react": "4.2.5",
|
||||
"agents": "^0.7.5",
|
||||
"aws-amplify": "^6.15.6",
|
||||
"ghostty-web": "^0.4.0",
|
||||
"bignumber.js": "^9.0.2",
|
||||
"chokidar": "3.5.1",
|
||||
"chrono-node": "2.2.4",
|
||||
@@ -167,7 +167,6 @@
|
||||
"fs": "0.0.1-security",
|
||||
"fs-extra": "9.1.0",
|
||||
"fuse.js": "6.4.6",
|
||||
"ghostty-web": "^0.4.0",
|
||||
"grapheme-splitter": "1.0.4",
|
||||
"graphology": "0.20.0",
|
||||
"hnswlib-wasm": "^0.8.2",
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
(ns frontend.components.agent-chat
|
||||
(:require ["@ai-sdk/react" :refer [useChat]]
|
||||
["agents/client" :refer [AgentClient]]
|
||||
["ghostty-web" :refer [FitAddon Terminal init]]
|
||||
[cljs-bean.core :as bean]
|
||||
[clojure.set :as set]
|
||||
[clojure.string :as string]
|
||||
[frontend.components.select :as select]
|
||||
[frontend.handler.agent :as agent-handler]
|
||||
[frontend.handler.agent-chat-transport :as chat-transport]
|
||||
[frontend.handler.db-based.sync :as db-sync]
|
||||
[frontend.handler.notification :as notification]
|
||||
[frontend.modules.agent-chat.event :as chat-event]
|
||||
[frontend.state :as state]
|
||||
[logseq.shui.hooks :as hooks]
|
||||
@@ -102,9 +99,7 @@
|
||||
|
||||
(defn- ^:large-vars/cleanup-todo session->messages
|
||||
[session block]
|
||||
(let [planning-messages (when (sequential? (:planning-messages session))
|
||||
(vec (:planning-messages session)))
|
||||
events (:events session)
|
||||
(let [events (:events session)
|
||||
base (let [acc (atom {:items {}
|
||||
:order []
|
||||
:item-kind-by-id {}
|
||||
@@ -371,18 +366,16 @@
|
||||
(append-text-part! item-id role delta))))))))]
|
||||
(doseq [event events]
|
||||
(process-event! event))
|
||||
(if (seq planning-messages)
|
||||
planning-messages
|
||||
(->> (:order @acc)
|
||||
(keep (fn [item-id]
|
||||
(let [entry (get-in @acc [:items item-id])
|
||||
parts (vec (keep normalize-message-part (:parts entry)))
|
||||
role (chat-role (:role entry))]
|
||||
(when (seq parts)
|
||||
{:id item-id
|
||||
:role role
|
||||
:parts parts}))))
|
||||
vec))))
|
||||
(->> (:order @acc)
|
||||
(keep (fn [item-id]
|
||||
(let [entry (get-in @acc [:items item-id])
|
||||
parts (vec (keep normalize-message-part (:parts entry)))
|
||||
role (chat-role (:role entry))]
|
||||
(when (seq parts)
|
||||
{:id item-id
|
||||
:role role
|
||||
:parts parts}))))
|
||||
vec)))
|
||||
task-text (some-> (or (:block/raw-title block) (:block/title block))
|
||||
string/trim)
|
||||
user-message (when-not (string/blank? task-text)
|
||||
@@ -434,48 +427,6 @@
|
||||
(let [single-line (string/replace summary #"\s+" " ")]
|
||||
(subs single-line 0 (min commit-message-max-len (count single-line))))))
|
||||
|
||||
(def ^:private planner-started-statuses
|
||||
#{"Doing" "Done" "In Review" "Canceled"})
|
||||
|
||||
(defn- planning-session-tasks
|
||||
[session session-chat-messages]
|
||||
(or (some-> session :plan :tasks seq vec)
|
||||
(some-> session-chat-messages
|
||||
latest-assistant-summary
|
||||
agent-handler/planner-tasks-from-text)))
|
||||
|
||||
(defn- planning-task-started?
|
||||
[task]
|
||||
(or (string? (normalized-text (:session-id task)))
|
||||
(contains? planner-started-statuses
|
||||
(some-> (:status task) normalized-text))))
|
||||
|
||||
(defn- executable-planner-tasks
|
||||
[tasks]
|
||||
(->> tasks
|
||||
(filter map?)
|
||||
(remove planning-task-started?)
|
||||
vec))
|
||||
|
||||
(defn- planning-task-title
|
||||
[task]
|
||||
(or (some-> (:title task) normalized-text)
|
||||
(some-> (:content task)
|
||||
normalized-text
|
||||
(string/split #"\n")
|
||||
first
|
||||
normalized-text)
|
||||
"Planned task"))
|
||||
|
||||
(defn- planning-task-status-label
|
||||
[task]
|
||||
(or (some-> (:status task) normalized-text)
|
||||
(when (string? (normalized-text (:session-id task)))
|
||||
"Doing")
|
||||
(when (string? (normalized-text (:block-uuid task)))
|
||||
"Todo")
|
||||
"Draft"))
|
||||
|
||||
(defn- session-messages-need-sync?
|
||||
[session-messages ui-messages]
|
||||
(let [ui-by-id (into {} (map (juxt :id identity) ui-messages))]
|
||||
@@ -618,9 +569,7 @@
|
||||
[block]
|
||||
(let [block-uuid (:block/uuid block)
|
||||
[sessions] (hooks/use-atom (:agent/sessions @state/state))
|
||||
[planning-sessions] (hooks/use-atom (:agent/planning-sessions @state/state))
|
||||
session (or (get planning-sessions (str block-uuid))
|
||||
(get sessions (str block-uuid)))
|
||||
session (get sessions (str block-uuid))
|
||||
session-id (or (:session-id session)
|
||||
(agent-handler/task-session-id block)
|
||||
(some-> block-uuid str))
|
||||
@@ -630,25 +579,12 @@
|
||||
pr-created? (string? task-pr-url)
|
||||
agent-value (:logseq.property/agent block)
|
||||
agent-label (agent-title agent-value)
|
||||
session-kind (some-> (:session-kind session) normalized-text)
|
||||
planning-chat-path (some-> (:planning-chat-path session) normalized-text)
|
||||
planning-session? (or (= "planning" session-kind)
|
||||
(string? planning-chat-path))
|
||||
planning-session-id (some-> (:planning-session-id session) normalized-text)
|
||||
execution-session? (not planning-session?)
|
||||
start-strategy (agent-handler/session-start-strategy block)
|
||||
message-endpoint (when planning-session?
|
||||
(or planning-chat-path
|
||||
(when (string? session-id)
|
||||
(str "/planning/chat/" session-id))))
|
||||
session-messages (session->messages session block)
|
||||
transport (hooks/use-memo
|
||||
#(chat-transport/make-transport {:base base
|
||||
:session-id session-id
|
||||
:open-stream? false
|
||||
:message-endpoint message-endpoint
|
||||
:stream-endpoint nil})
|
||||
[base session-id message-endpoint])
|
||||
:open-stream? false})
|
||||
[base session-id])
|
||||
chat (useChat #js {:id session-id
|
||||
:transport transport
|
||||
:messages (clj->js session-messages)})
|
||||
@@ -678,15 +614,10 @@
|
||||
[loading-branches? set-loading-branches?!] (rum/use-state false)
|
||||
[starting-session? set-starting-session?!] (rum/use-state false)
|
||||
[publish-mode set-publish-mode!] (rum/use-state nil)
|
||||
[creating-tasks? set-creating-tasks?!] (rum/use-state false)
|
||||
[planning-action-mode set-planning-action-mode!] (rum/use-state nil)
|
||||
[planning-note set-planning-note!] (rum/use-state "")
|
||||
[selected-task-uuids set-selected-task-uuids!] (rum/use-state #{})
|
||||
[terminal-visible? set-terminal-visible!] (rum/use-state false)
|
||||
[terminal-status set-terminal-status!] (rum/use-state :idle)
|
||||
[terminal-error set-terminal-error!] (rum/use-state nil)
|
||||
[terminal-connection-key set-terminal-connection-key!] (rum/use-state 0)
|
||||
planning-client-ref (hooks/use-ref nil)
|
||||
terminal-container-ref (hooks/use-ref nil)
|
||||
auth-token (state/get-auth-id-token)
|
||||
terminal-url (agent-handler/terminal-websocket-url base
|
||||
@@ -695,16 +626,6 @@
|
||||
terminal-open-disabled? (or (not session-started?)
|
||||
(not (string? terminal-url)))
|
||||
trimmed-draft (string/trim (or draft ""))
|
||||
trimmed-planning-note (string/trim (or planning-note ""))
|
||||
planner-tasks (planning-session-tasks session session-chat-messages)
|
||||
executable-planner-task-list (executable-planner-tasks planner-tasks)
|
||||
selectable-task-uuids (->> executable-planner-task-list
|
||||
(keep (fn [task]
|
||||
(some-> (:task-uuid task) normalized-text)))
|
||||
set)
|
||||
selected-planner-task-uuids (if (empty? selectable-task-uuids)
|
||||
#{}
|
||||
(set/intersection selected-task-uuids selectable-task-uuids))
|
||||
selected-start-branch (normalized-text start-branch)
|
||||
branch-select-items (vec (map (fn [branch]
|
||||
{:value branch
|
||||
@@ -718,20 +639,6 @@
|
||||
(not (string? selected-start-branch))
|
||||
(not (agent-handler/task-ready? block)))
|
||||
publish-disabled? (or input-disabled? busy? publish-busy?)
|
||||
create-tasks-disabled? (or creating-tasks? busy? (empty? planner-tasks))
|
||||
planning-action-busy? (some? planning-action-mode)
|
||||
approve-disabled? (or planning-action-busy?
|
||||
(not planning-session?)
|
||||
(= "approved" (:approval-status session)))
|
||||
reject-disabled? (or planning-action-busy?
|
||||
(not planning-session?)
|
||||
(= "rejected" (:approval-status session)))
|
||||
replan-disabled? (or planning-action-busy?
|
||||
(not planning-session?))
|
||||
execute-selected-disabled? (or planning-action-busy?
|
||||
creating-tasks?
|
||||
(empty? planner-tasks)
|
||||
(empty? selected-planner-task-uuids))
|
||||
can-send? (and (not input-disabled?)
|
||||
(not (string/blank? trimmed-draft))
|
||||
(not busy?))
|
||||
@@ -739,20 +646,13 @@
|
||||
send-message! (fn []
|
||||
(when (and can-send? base session-id)
|
||||
(set-draft! "")
|
||||
(if planning-session?
|
||||
(do
|
||||
(agent-handler/append-planning-message! block-uuid "user" trimmed-draft)
|
||||
(when-let [client (hooks/deref planning-client-ref)]
|
||||
(.send client
|
||||
(js/JSON.stringify
|
||||
(clj->js {:content trimmed-draft})))))
|
||||
(-> (.sendMessage chat #js {:text trimmed-draft})
|
||||
(.catch (fn [_] nil))))))
|
||||
(-> (.sendMessage chat #js {:text trimmed-draft})
|
||||
(.catch (fn [_] nil)))))
|
||||
start-session! (fn []
|
||||
(when (and base session-id (not start-session-disabled?))
|
||||
(set-starting-session?! true)
|
||||
(let [opts {:base-branch selected-start-branch}]
|
||||
(-> (agent-handler/<start-auto-session! block opts)
|
||||
(-> (agent-handler/<start-session! block opts)
|
||||
(p/then (fn [resp]
|
||||
(when (and (map? resp)
|
||||
(string? selected-start-branch)
|
||||
@@ -787,82 +687,6 @@
|
||||
(-> (agent-handler/<publish-session! block opts)
|
||||
(p/catch (fn [_] nil))
|
||||
(p/finally (fn [] (set-publish-mode! nil)))))))
|
||||
create-tasks! (fn []
|
||||
(when-not create-tasks-disabled?
|
||||
(set-creating-tasks?! true)
|
||||
(-> (agent-handler/<upsert-planner-tasks! block-uuid
|
||||
planner-tasks
|
||||
(cond-> {}
|
||||
(string? planning-session-id)
|
||||
(assoc :planning-session-id planning-session-id)))
|
||||
(p/then (fn [results]
|
||||
(notification/show! (str "Synced " (count results) " task"
|
||||
(when (not= 1 (count results)) "s")
|
||||
".")
|
||||
:success false)))
|
||||
(p/catch (fn [error]
|
||||
(notification/show! (or (some-> error ex-message)
|
||||
"Failed to create tasks.")
|
||||
:error false)
|
||||
nil))
|
||||
(p/finally (fn [] (set-creating-tasks?! false))))))
|
||||
planning-action! (fn [mode f success-message error-message]
|
||||
(set-planning-action-mode! mode)
|
||||
(-> (f)
|
||||
(p/then (fn [result]
|
||||
(when (string? success-message)
|
||||
(notification/show! success-message :success false))
|
||||
result))
|
||||
(p/catch (fn [error]
|
||||
(notification/show! (or (some-> error ex-message)
|
||||
error-message)
|
||||
:error false)
|
||||
nil))
|
||||
(p/finally (fn [] (set-planning-action-mode! nil)))))
|
||||
maybe-send-planning-note! (fn []
|
||||
(when (and planning-session?
|
||||
(not (string/blank? trimmed-planning-note)))
|
||||
(agent-handler/append-planning-message! block-uuid "user" trimmed-planning-note)
|
||||
(when-let [client (hooks/deref planning-client-ref)]
|
||||
(.send client
|
||||
(js/JSON.stringify
|
||||
(clj->js {:content trimmed-planning-note}))))))
|
||||
approve-plan! (fn []
|
||||
(planning-action! :approve
|
||||
(fn []
|
||||
(agent-handler/<set-planning-approval! block-uuid
|
||||
"approved"
|
||||
{:comment trimmed-planning-note}))
|
||||
"Planning approved."
|
||||
"Failed to approve planning."))
|
||||
reject-plan! (fn []
|
||||
(planning-action! :reject
|
||||
(fn []
|
||||
(agent-handler/<set-planning-approval! block-uuid
|
||||
"rejected"
|
||||
{:comment trimmed-planning-note}))
|
||||
"Planning rejected."
|
||||
"Failed to reject planning."))
|
||||
replan! (fn []
|
||||
(planning-action! :replan
|
||||
(fn []
|
||||
(maybe-send-planning-note!)
|
||||
(agent-handler/<replan-planning-session! block-uuid
|
||||
{:replan-note trimmed-planning-note}))
|
||||
"Replanning queued."
|
||||
"Failed to queue replanning."))
|
||||
execute-selected! (fn []
|
||||
(when-not execute-selected-disabled?
|
||||
(planning-action! :execute
|
||||
(fn []
|
||||
(agent-handler/<execute-planned-tasks! block-uuid
|
||||
planner-tasks
|
||||
{:planning-session-id planning-session-id
|
||||
:selected-task-uuids selected-planner-task-uuids}))
|
||||
(str "Started " (count selected-planner-task-uuids) " planned task"
|
||||
(when (not= 1 (count selected-planner-task-uuids)) "s")
|
||||
".")
|
||||
"Failed to start planned tasks.")))
|
||||
open-terminal! (fn []
|
||||
(when (and terminal-enabled? (not terminal-open-disabled?))
|
||||
(set-terminal-visible! true)
|
||||
@@ -892,48 +716,6 @@
|
||||
(agent-handler/<ensure-session! block))
|
||||
nil)
|
||||
[block-uuid (:logseq.property/project block) (:logseq.property/agent block) session-created?])
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
(when-let [client (hooks/deref planning-client-ref)]
|
||||
(.close client))
|
||||
(hooks/set-ref! planning-client-ref nil)
|
||||
(when (and planning-session?
|
||||
base
|
||||
(string? planning-session-id)
|
||||
(string? auth-token))
|
||||
(let [client (new AgentClient
|
||||
(clj->js {:agent "PlanningSessionAgent"
|
||||
:basePath (str "planning/chat/" planning-session-id)
|
||||
:host base
|
||||
:query {:token auth-token}
|
||||
:onMessage (fn [event]
|
||||
(when-let [raw (some-> event .-data)]
|
||||
(when-let [message (parse-json-safe raw)]
|
||||
(case (:type message)
|
||||
"planning.state"
|
||||
(agent-handler/replace-planning-messages! block-uuid
|
||||
(:state message))
|
||||
|
||||
"planning.message"
|
||||
(agent-handler/replace-planning-messages! block-uuid
|
||||
(:state message))
|
||||
|
||||
nil))))}))]
|
||||
(hooks/set-ref! planning-client-ref client)))
|
||||
(fn []
|
||||
(when-let [client (hooks/deref planning-client-ref)]
|
||||
(.close client))
|
||||
(hooks/set-ref! planning-client-ref nil)))
|
||||
[planning-session? planning-session-id auth-token base block-uuid])
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
(when planning-session?
|
||||
(set-selected-task-uuids! selectable-task-uuids))
|
||||
nil)
|
||||
[planning-session?
|
||||
(pr-str (mapv (fn [task]
|
||||
(select-keys task [:task-uuid :block-uuid :session-id :status]))
|
||||
planner-tasks))])
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
(let [alive? (atom true)
|
||||
@@ -978,10 +760,10 @@
|
||||
[session-id (pr-str session-chat-messages) (pr-str chat-messages)])
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
(when (and base session-id session-started? (not planning-session?))
|
||||
(when (and base session-id session-started?)
|
||||
(agent-handler/<fetch-events! block))
|
||||
nil)
|
||||
[session-id session-started? planning-session?])
|
||||
[session-id session-started?])
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
(when (and terminal-visible?
|
||||
@@ -993,6 +775,7 @@
|
||||
dispose-data* (atom nil)
|
||||
input-seq* (atom 0)
|
||||
socket (js/WebSocket. terminal-url)
|
||||
encoder (js/TextEncoder.)
|
||||
handle-window-resize (fn []
|
||||
(when-let [fit-addon @fit-addon*]
|
||||
(try
|
||||
@@ -1157,11 +940,7 @@
|
||||
:on-click (fn [_] (start-session!))}
|
||||
(if starting-session?
|
||||
"Starting..."
|
||||
(if (:planning? start-strategy)
|
||||
"Start agent"
|
||||
"Start agent")))
|
||||
[:div.mt-1.text-xs.opacity-70
|
||||
(:reason start-strategy)]]]
|
||||
"Start session"))]]
|
||||
[:<>
|
||||
[:div.flex.flex-wrap.items-center.gap-2
|
||||
[:div {:class "inline-flex w-fit items-center gap-1 rounded-lg border border-border bg-muted/40 p-1"}
|
||||
@@ -1209,67 +988,13 @@
|
||||
{:size :sm
|
||||
:variant :outline
|
||||
:class "h-7 px-2 text-xs"
|
||||
:disabled create-tasks-disabled?
|
||||
:disabled publish-disabled?
|
||||
:on-click (fn [_]
|
||||
(create-tasks!))}
|
||||
(if creating-tasks?
|
||||
"Syncing tasks..."
|
||||
(if planning-session? "Sync tasks" "Create tasks")))
|
||||
(when planning-session?
|
||||
(shui/button
|
||||
{:size :sm
|
||||
:variant :outline
|
||||
:class "h-7 px-2 text-xs"
|
||||
:disabled execute-selected-disabled?
|
||||
:on-click (fn [_]
|
||||
(execute-selected!))}
|
||||
(if (= planning-action-mode :execute)
|
||||
"Starting..."
|
||||
"Execute selected")))
|
||||
(when planning-session?
|
||||
(shui/button
|
||||
{:size :sm
|
||||
:variant :outline
|
||||
:class "h-7 px-2 text-xs"
|
||||
:disabled reject-disabled?
|
||||
:on-click (fn [_]
|
||||
(reject-plan!))}
|
||||
(if (= planning-action-mode :reject)
|
||||
"Rejecting..."
|
||||
"Reject")))
|
||||
(when planning-session?
|
||||
(shui/button
|
||||
{:size :sm
|
||||
:variant :outline
|
||||
:class "h-7 px-2 text-xs"
|
||||
:disabled replan-disabled?
|
||||
:on-click (fn [_]
|
||||
(replan!))}
|
||||
(if (= planning-action-mode :replan)
|
||||
"Replanning..."
|
||||
"Replan")))
|
||||
(when planning-session?
|
||||
(shui/button
|
||||
{:size :sm
|
||||
:class "h-7 px-2 text-xs"
|
||||
:disabled approve-disabled?
|
||||
:on-click (fn [_]
|
||||
(approve-plan!))}
|
||||
(if (= planning-action-mode :approve)
|
||||
"Approving..."
|
||||
"Approve")))
|
||||
(when execution-session?
|
||||
(shui/button
|
||||
{:size :sm
|
||||
:variant :outline
|
||||
:class "h-7 px-2 text-xs"
|
||||
:disabled publish-disabled?
|
||||
:on-click (fn [_]
|
||||
(publish! false))}
|
||||
(if (= publish-mode :push)
|
||||
"Pushing..."
|
||||
"Push")))
|
||||
(when (and execution-session? (not pr-created?))
|
||||
(publish! false))}
|
||||
(if (= publish-mode :push)
|
||||
"Pushing..."
|
||||
"Push"))
|
||||
(when-not pr-created?
|
||||
(shui/button
|
||||
{:size :sm
|
||||
:class "h-7 px-2 text-xs"
|
||||
@@ -1279,72 +1004,6 @@
|
||||
(if (= publish-mode :pr)
|
||||
"Creating PR..."
|
||||
"Push + PR")))]
|
||||
(when planning-session?
|
||||
[:div {:class "rounded-xl border border-border/70 bg-muted/20 p-3"}
|
||||
[:div.flex.flex-col.gap-3
|
||||
[:div.flex.flex-wrap.items-center.justify-between.gap-2
|
||||
[:div.text-xs.opacity-70
|
||||
(str "Approval: " (or (:approval-status session) "pending")
|
||||
" | Tasks: " (count planner-tasks)
|
||||
" | Selected: " (count selected-planner-task-uuids))]
|
||||
(when (seq selectable-task-uuids)
|
||||
[:div.flex.items-center.gap-2.text-xs
|
||||
(shui/button
|
||||
{:size :sm
|
||||
:variant :ghost
|
||||
:class "h-7 px-2 text-xs"
|
||||
:on-click (fn [_]
|
||||
(set-selected-task-uuids! selectable-task-uuids))}
|
||||
"Select all")
|
||||
(shui/button
|
||||
{:size :sm
|
||||
:variant :ghost
|
||||
:class "h-7 px-2 text-xs"
|
||||
:on-click (fn [_]
|
||||
(set-selected-task-uuids! #{}))}
|
||||
"Clear")])]
|
||||
[:textarea.w-full.rounded-lg.border.border-border.bg-background.p-2.text-sm.outline-none
|
||||
{:rows 3
|
||||
:value planning-note
|
||||
:placeholder "Add approval notes or replanning feedback..."
|
||||
:on-change (fn [e]
|
||||
(set-planning-note! (.. e -target -value)))}]
|
||||
(if (seq planner-tasks)
|
||||
[:div.flex.flex-col.gap-2
|
||||
(for [task planner-tasks
|
||||
:let [task-uuid (some-> (:task-uuid task) normalized-text)
|
||||
selectable? (and (string? task-uuid)
|
||||
(contains? selectable-task-uuids task-uuid))
|
||||
checked? (and selectable?
|
||||
(contains? selected-planner-task-uuids task-uuid))]]
|
||||
[:label {:key (or task-uuid (str "planned-task-" (random-uuid)))
|
||||
:class "flex items-start gap-3 rounded-lg border border-border/60 bg-background/70 px-3 py-2"}
|
||||
[:input.mt-1
|
||||
{:type "checkbox"
|
||||
:disabled (not selectable?)
|
||||
:checked (boolean checked?)
|
||||
:on-change (fn [e]
|
||||
(let [checked (.. e -target -checked)]
|
||||
(set-selected-task-uuids!
|
||||
(if checked
|
||||
(conj selected-planner-task-uuids task-uuid)
|
||||
(disj selected-planner-task-uuids task-uuid)))))}]
|
||||
[:div.min-w-0.flex-1
|
||||
[:div.flex.flex-wrap.items-center.gap-2
|
||||
[:div.font-medium (planning-task-title task)]
|
||||
[:div {:class "rounded-full bg-muted px-2 py-0.5 text-[10px] uppercase tracking-wide opacity-70"}
|
||||
(planning-task-status-label task)]]
|
||||
(when-let [description (some-> (:description task) normalized-text)]
|
||||
[:div.mt-1.text-xs.opacity-80 description])
|
||||
[:div {:class "mt-1 flex flex-wrap items-center gap-2 text-[10px] opacity-60"}
|
||||
(when-let [task-uuid task-uuid]
|
||||
[:span (str "task " task-uuid)])
|
||||
(when-let [block-id (some-> (:block-uuid task) str normalized-text)]
|
||||
[:span (str "block " block-id)])
|
||||
(when-let [task-session-id (some-> (:session-id task) normalized-text)]
|
||||
[:span (str "session " task-session-id)])]]])]
|
||||
[:div.text-xs.opacity-70
|
||||
"Planning tasks will appear here once the workflow produces a persisted plan."])]])
|
||||
(when (string? error)
|
||||
[:div {:class "mt-0.5 rounded-lg border border-red-300/40 bg-red-500/5 px-3 py-1.5 text-xs text-red-500"}
|
||||
error])
|
||||
|
||||
@@ -71,9 +71,6 @@
|
||||
(goog-define ENABLE-PLUGINS true)
|
||||
(defonce feature-plugin-system-on? ENABLE-PLUGINS)
|
||||
|
||||
(goog-define ENABLE-AGENT-PLANNING false)
|
||||
(defonce feature-agent-planning-on? ENABLE-AGENT-PLANNING)
|
||||
|
||||
;; Desktop only as other platforms requires better understanding of their
|
||||
;; multi-graph workflows and optimal place for a "global" dir
|
||||
(def global-config-enabled? util/electron?)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"Agent sessions for tasks."
|
||||
(:require [clojure.string :as string]
|
||||
[electron.ipc :as electron-ipc]
|
||||
[frontend.config :as config]
|
||||
[frontend.db :as db]
|
||||
[frontend.handler.agent-cancel :as agent-cancel]
|
||||
[frontend.handler.db-based.sync :as db-sync]
|
||||
@@ -40,25 +39,7 @@
|
||||
(let [value (string/trim value)]
|
||||
(when-not (string/blank? value) value))))
|
||||
|
||||
(defn- parse-uuid-safe
|
||||
[value]
|
||||
(cond
|
||||
(uuid? value) value
|
||||
(string? value)
|
||||
(try
|
||||
(uuid value)
|
||||
(catch :default _
|
||||
nil))
|
||||
:else nil))
|
||||
|
||||
(declare <start-session!
|
||||
task-session-created?
|
||||
task-session-id
|
||||
task-pr-url
|
||||
maybe-store-task-session-id!
|
||||
session-state
|
||||
update-session!
|
||||
update-session-state!)
|
||||
(declare <start-session!)
|
||||
|
||||
(def ^:private task-sandbox-checkpoint-property :logseq.property/sandbox-checkpoint)
|
||||
(def ^:private task-session-id-property :logseq.property/agent-session-id)
|
||||
@@ -89,7 +70,7 @@
|
||||
(normalize-sandbox-checkpoint checkpoint))
|
||||
|
||||
(defn- agent-config
|
||||
[agent-page opts]
|
||||
[agent-page]
|
||||
(let [provider (blank->nil (:block/title agent-page))]
|
||||
(cond-> {}
|
||||
(string? provider) (assoc :provider provider))))
|
||||
@@ -157,10 +138,7 @@
|
||||
(task-sandbox-checkpoint project-page)))
|
||||
agent-page (:logseq.property/agent block)
|
||||
project (when project-page (project-config project-page opts))
|
||||
agent (when agent-page
|
||||
(cond-> (agent-config agent-page opts)
|
||||
(string? (blank->nil (:agent/mode opts))) (assoc :mode (blank->nil (:agent/mode opts)))
|
||||
(string? (blank->nil (:agent/permission-mode opts))) (assoc :permission-mode (blank->nil (:agent/permission-mode opts)))))]
|
||||
agent (when agent-page (agent-config agent-page))]
|
||||
{:block-uuid block-uuid
|
||||
:node-id node-id
|
||||
:node-title node-title
|
||||
@@ -190,50 +168,6 @@
|
||||
(and (map? agent)
|
||||
(codex-agent? agent))))
|
||||
|
||||
(defn planning-enabled?
|
||||
[]
|
||||
(or config/dev?
|
||||
config/feature-agent-planning-on?
|
||||
(user-handler/alpha-or-beta-user?)))
|
||||
|
||||
(def ^:private planning-intent-pattern
|
||||
#"(?i)\b(plan|planning|roadmap|break down|decompose|architecture|research|investigate|clarify|phases?|milestones?|workstreams?)\b")
|
||||
|
||||
(def ^:private execution-intent-pattern
|
||||
#"(?i)\b(implement|build|create|fix|refactor|update|change|add|remove|debug|repair|ship)\b")
|
||||
|
||||
(defn session-start-strategy
|
||||
[block]
|
||||
(let [{:keys [content]} (task-context block)
|
||||
content (blank->nil content)
|
||||
lines (if (string? content)
|
||||
(count (string/split-lines content))
|
||||
0)
|
||||
content-length (count (or content ""))
|
||||
planning-signals (cond-> 0
|
||||
(and (string? content)
|
||||
(re-find planning-intent-pattern content)) inc
|
||||
(> lines 6) inc
|
||||
(> content-length 260) inc
|
||||
(and (string? content)
|
||||
(>= (count (re-seq #"\band\b|," content)) 3)) inc)
|
||||
_ (prn :debug :planning-signals planning-signals
|
||||
:content content)
|
||||
execution-signal? (and (string? content)
|
||||
(re-find execution-intent-pattern content))
|
||||
planning? (and (planning-enabled?)
|
||||
(or (>= planning-signals 2)
|
||||
(and (>= planning-signals 1)
|
||||
(not execution-signal?))))]
|
||||
{:mode (if planning? :planning :execution)
|
||||
:planning? planning?
|
||||
:reason (cond
|
||||
planning?
|
||||
"Task looks broad enough to benefit from planning first."
|
||||
|
||||
:else
|
||||
"Task looks concrete enough to execute directly.")}))
|
||||
|
||||
(defn- login-required-error?
|
||||
[error]
|
||||
(let [status (:status (ex-data error))
|
||||
@@ -382,8 +316,7 @@
|
||||
(build-session-body block nil))
|
||||
([block opts]
|
||||
(let [{:keys [block-uuid node-id node-title content attachments sandbox-checkpoint project agent]} (task-context block opts)
|
||||
session-id (or (some-> (:session-id opts) blank->nil)
|
||||
(some-> block-uuid str))]
|
||||
session-id (some-> block-uuid str)]
|
||||
(when (and session-id node-id (string? node-title) (string? content) (map? project) (map? agent))
|
||||
(cond-> {:session-id session-id
|
||||
:node-id node-id
|
||||
@@ -396,335 +329,6 @@
|
||||
:pr-enabled true}}
|
||||
(map? sandbox-checkpoint) (assoc :sandbox-checkpoint sandbox-checkpoint))))))
|
||||
|
||||
(def ^:private planner-default-status :logseq.property/status.todo)
|
||||
|
||||
(defn- parse-block-uuid
|
||||
[value]
|
||||
(cond
|
||||
(uuid? value) value
|
||||
(string? value)
|
||||
(try
|
||||
(uuid value)
|
||||
(catch :default _ nil))
|
||||
:else nil))
|
||||
|
||||
(defn- planner-task-title
|
||||
[{:keys [title content description]}]
|
||||
(or (blank->nil title)
|
||||
(some-> (or (blank->nil content)
|
||||
(blank->nil description))
|
||||
(string/split #"\n")
|
||||
first
|
||||
blank->nil)))
|
||||
|
||||
(defn- planner-task-content
|
||||
[{:keys [content title description]}]
|
||||
(or (blank->nil content)
|
||||
(let [title (blank->nil title)
|
||||
description (blank->nil description)]
|
||||
(cond
|
||||
(and title description) (str title "\n" description)
|
||||
title title
|
||||
description description
|
||||
:else nil))))
|
||||
|
||||
(defn- unwrap-json-code-fence
|
||||
[text]
|
||||
(when-let [text (blank->nil text)]
|
||||
(or (some->> text
|
||||
(re-find #"(?is)```(?:json)?\s*\n(.*?)\n```")
|
||||
second
|
||||
blank->nil)
|
||||
text)))
|
||||
|
||||
(defn- parse-json-safe
|
||||
[text]
|
||||
(when-let [text (unwrap-json-code-fence text)]
|
||||
(try
|
||||
(js->clj (js/JSON.parse text) :keywordize-keys true)
|
||||
(catch :default _ nil))))
|
||||
|
||||
(defn planner-tasks-from-text
|
||||
[text]
|
||||
(let [parsed (parse-json-safe text)
|
||||
tasks (cond
|
||||
(vector? parsed) parsed
|
||||
(sequential? (:tasks parsed)) (vec (:tasks parsed))
|
||||
:else nil)]
|
||||
(when (seq tasks)
|
||||
(->> tasks
|
||||
(keep (fn [task]
|
||||
(when (map? task)
|
||||
(let [task' (cond-> {}
|
||||
(parse-block-uuid (:block-uuid task)) (assoc :block-uuid (parse-block-uuid (:block-uuid task)))
|
||||
(string? (blank->nil (:title task))) (assoc :title (blank->nil (:title task)))
|
||||
(string? (blank->nil (:description task))) (assoc :description (blank->nil (:description task)))
|
||||
(string? (blank->nil (:content task))) (assoc :content (blank->nil (:content task))))]
|
||||
(when (planner-task-content task')
|
||||
task')))))
|
||||
vec
|
||||
seq
|
||||
vec))))
|
||||
|
||||
(defn- planning-summary-task
|
||||
[task]
|
||||
(when (map? task)
|
||||
(let [title (blank->nil (:title task))
|
||||
description (blank->nil (:description task))
|
||||
content (blank->nil (:content task))
|
||||
task-uuid (blank->nil (:task-uuid task))
|
||||
block-uuid (blank->nil (:block-uuid task))
|
||||
session-id (blank->nil (:session-id task))]
|
||||
(cond-> {}
|
||||
(string? title) (assoc :title title)
|
||||
(string? description) (assoc :description description)
|
||||
(string? content) (assoc :content content)
|
||||
(string? task-uuid) (assoc :task-uuid task-uuid)
|
||||
(string? block-uuid) (assoc :block-uuid block-uuid)
|
||||
(string? session-id) (assoc :session-id session-id)))))
|
||||
|
||||
(defn- planning-session-summary
|
||||
[planning-session]
|
||||
(let [tasks (or (some-> planning-session :plan :tasks)
|
||||
[])
|
||||
tasks (->> tasks
|
||||
(keep planning-summary-task)
|
||||
vec)
|
||||
status (blank->nil (:status planning-session))
|
||||
approval-status (blank->nil (:approval-status planning-session))
|
||||
workflow-id (blank->nil (:workflow-id planning-session))
|
||||
scheduled-actions (or (:scheduled-actions planning-session) [])
|
||||
header-lines (cond-> []
|
||||
(string? status) (conj (str "Planning status: " status "."))
|
||||
(string? approval-status) (conj (str "Approval status: " approval-status "."))
|
||||
(string? workflow-id) (conj (str "Workflow id: " workflow-id "."))
|
||||
(seq scheduled-actions) (conj (str "Scheduled actions: "
|
||||
(pr-str scheduled-actions)
|
||||
".")))]
|
||||
(when (or (seq tasks) (seq header-lines))
|
||||
(str (string/join "\n" header-lines)
|
||||
(when (seq header-lines) "\n\n")
|
||||
"```json\n"
|
||||
(js/JSON.stringify (clj->js {:tasks tasks}) nil 2)
|
||||
"\n```"))))
|
||||
|
||||
(defn- planning-summary-messages
|
||||
[planning-session]
|
||||
(when-let [summary (planning-session-summary planning-session)]
|
||||
[{:id (str "planning-summary-" (or (:planning-session-id planning-session)
|
||||
(random-uuid)))
|
||||
:role "assistant"
|
||||
:parts [{:type "text"
|
||||
:text summary}]}]))
|
||||
|
||||
(defn- planning-chat-message
|
||||
[message]
|
||||
(let [content (blank->nil (:content message))
|
||||
role (blank->nil (:role message))
|
||||
message-id (blank->nil (:id message))]
|
||||
(when (and (string? content)
|
||||
(string? role))
|
||||
{:id (or message-id (str "planning-message-" (random-uuid)))
|
||||
:role role
|
||||
:parts [{:type "text"
|
||||
:text content}]})))
|
||||
|
||||
(defn- planning-chat-messages
|
||||
[planning-state]
|
||||
(->> (:messages planning-state)
|
||||
(keep planning-chat-message)
|
||||
vec))
|
||||
|
||||
(defn- planning-summary-message?
|
||||
[message]
|
||||
(string/starts-with? (or (:id message) "") "planning-summary-"))
|
||||
|
||||
(defn- merge-planning-summary-messages
|
||||
[block-uuid planning-session]
|
||||
(let [existing-messages (or (get-in (session-state block-uuid) [:planning-messages]) [])
|
||||
non-summary (remove planning-summary-message? existing-messages)]
|
||||
(into (or (planning-summary-messages planning-session) [])
|
||||
non-summary)))
|
||||
|
||||
(defn- planning-session-state-update
|
||||
[block-uuid planning-session]
|
||||
(let [planning-session-id (or (:planning-session-id planning-session)
|
||||
(:session-id planning-session))
|
||||
session-id (blank->nil planning-session-id)]
|
||||
{:session-id session-id
|
||||
:session-kind "planning"
|
||||
:planning-session-id session-id
|
||||
:planning-chat-path (or (blank->nil (:chat-path planning-session))
|
||||
(when (string? session-id)
|
||||
(str "/planning/chat/" session-id)))
|
||||
:workflow-id (:workflow-id planning-session)
|
||||
:status (:status planning-session)
|
||||
:plan (:plan planning-session)
|
||||
:dispatch-sessions (:dispatch-sessions planning-session)
|
||||
:scheduled-actions (:scheduled-actions planning-session)
|
||||
:approval-status (:approval-status planning-session)
|
||||
:require-approval (true? (:require-approval planning-session))
|
||||
:auto-dispatch (if (boolean? (:auto-dispatch planning-session))
|
||||
(:auto-dispatch planning-session)
|
||||
true)
|
||||
:auto-replan (true? (:auto-replan planning-session))
|
||||
:replan-delay-sec (:replan-delay-sec planning-session)
|
||||
:planning-messages (merge-planning-summary-messages block-uuid planning-session)
|
||||
:planning-loaded? true
|
||||
:planning-loading? false}))
|
||||
|
||||
(defn- enrich-planning-task-state
|
||||
[task]
|
||||
(if-let [block (some-> (:block-uuid task) parse-uuid-safe (vector :block/uuid) db/entity)]
|
||||
(cond-> task
|
||||
true (assoc :status (or (pu/get-block-property-value block :logseq.property/status)
|
||||
(:status task)))
|
||||
(string? (task-session-id block)) (assoc :session-id (task-session-id block))
|
||||
(string? (task-pr-url block)) (assoc :pr-url (task-pr-url block)))
|
||||
task))
|
||||
|
||||
(defn- apply-planning-session!
|
||||
[block-uuid planning-session]
|
||||
(when (map? planning-session)
|
||||
(let [planning-session (update-in planning-session [:plan :tasks]
|
||||
(fn [tasks]
|
||||
(if (sequential? tasks)
|
||||
(mapv enrich-planning-task-state tasks)
|
||||
tasks)))]
|
||||
(doseq [task (get-in planning-session [:plan :tasks])]
|
||||
(when (and (:block-uuid task) (:session-id task))
|
||||
(maybe-store-task-session-id! (:block-uuid task) (:session-id task))))
|
||||
(update-session-state! block-uuid
|
||||
(planning-session-state-update block-uuid planning-session)))
|
||||
planning-session))
|
||||
|
||||
(defn replace-planning-messages!
|
||||
[block-uuid planning-state]
|
||||
(let [existing-summary (->> (get-in (session-state block-uuid) [:planning-messages])
|
||||
(filter #(string/starts-with? (or (:id %) "") "planning-summary-"))
|
||||
vec)
|
||||
messages (into existing-summary (planning-chat-messages planning-state))]
|
||||
(update-session-state! block-uuid {:planning-messages messages
|
||||
:planning-agent-state planning-state})))
|
||||
|
||||
(defn append-planning-message!
|
||||
[block-uuid role content]
|
||||
(when-let [content (blank->nil content)]
|
||||
(update-session! block-uuid
|
||||
(fn [session]
|
||||
(let [messages (vec (or (:planning-messages session) []))]
|
||||
(assoc session
|
||||
:planning-messages
|
||||
(conj messages {:id (str "planning-local-" (random-uuid))
|
||||
:role role
|
||||
:parts [{:type "text"
|
||||
:text content}]}))))
|
||||
:agent/planning-sessions)))
|
||||
|
||||
(defn- planner-goal-context
|
||||
[goal-block {:keys [project agent] :as _opts}]
|
||||
{:project (or project (:logseq.property/project goal-block))
|
||||
:agent (or agent (:logseq.property/agent goal-block))})
|
||||
|
||||
(defn- task-tagged?
|
||||
[block class-ident]
|
||||
(boolean
|
||||
(some #(= class-ident (:db/ident %))
|
||||
(:block/tags block))))
|
||||
|
||||
(defn- maybe-set-planner-task-defaults!
|
||||
[block-uuid {:keys [project agent]}]
|
||||
(when-let [block (db/entity [:block/uuid block-uuid])]
|
||||
(when-not (task-tagged? block :logseq.class/Task)
|
||||
(property-handler/set-block-property! block-uuid :block/tags :logseq.class/Task))
|
||||
(when-not (pu/get-block-property-value block :logseq.property/status)
|
||||
(property-handler/set-block-property! block-uuid :logseq.property/status planner-default-status))
|
||||
(when (and project
|
||||
(not= (:db/id (:logseq.property/project block))
|
||||
(:db/id project)))
|
||||
(property-handler/set-block-property! block-uuid :logseq.property/project (:block/title project)))
|
||||
(when (and agent
|
||||
(not= (:db/id (:logseq.property/agent block))
|
||||
(:db/id agent)))
|
||||
(property-handler/set-block-property! block-uuid :logseq.property/agent (:block/title agent)))))
|
||||
|
||||
(defn- maybe-update-planner-task-content!
|
||||
[block task]
|
||||
(when-let [content (planner-task-content task)]
|
||||
(when (not= (:block/title block) content)
|
||||
(editor-handler/save-block! (state/get-current-repo) (:block/uuid block) content))))
|
||||
|
||||
(defn- <create-planner-task!
|
||||
[goal-block anchor-block-uuid task context]
|
||||
(let [target-block-id (or anchor-block-uuid (:db/id goal-block))
|
||||
sibling? (some? anchor-block-uuid)
|
||||
content (planner-task-content task)]
|
||||
(if-not (and target-block-id (string? content))
|
||||
(p/resolved nil)
|
||||
(p/let [result (editor-handler/insert-block-tree-after-target target-block-id sibling? [{:content content}] :markdown false)
|
||||
block-uuid (some-> result :blocks first :block/uuid)]
|
||||
(when block-uuid
|
||||
(maybe-set-planner-task-defaults! block-uuid context)
|
||||
{:block-uuid block-uuid})))))
|
||||
|
||||
(defn- <upsert-planner-task!
|
||||
[goal-block anchor-block-uuid task context]
|
||||
(if-let [block (when-let [block-uuid (:block-uuid task)]
|
||||
(db/entity [:block/uuid block-uuid]))]
|
||||
(if (task-session-created? block)
|
||||
(p/resolved {:block-uuid (:block/uuid block)})
|
||||
(do
|
||||
(maybe-update-planner-task-content! block task)
|
||||
(maybe-set-planner-task-defaults! (:block/uuid block) context)
|
||||
(p/resolved {:block-uuid (:block/uuid block)})))
|
||||
(<create-planner-task! goal-block anchor-block-uuid task context)))
|
||||
|
||||
(defn- <sync-planning-task-bindings!
|
||||
[planning-session-id tasks]
|
||||
(let [base (db-sync/http-base)
|
||||
bindings (->> tasks
|
||||
(keep (fn [task]
|
||||
(let [task-uuid (blank->nil (:task-uuid task))
|
||||
block-uuid (some-> (:block-uuid task) str blank->nil)
|
||||
session-id (blank->nil (:session-id task))]
|
||||
(when (and (string? task-uuid)
|
||||
(string? block-uuid))
|
||||
(cond-> {:task-uuid task-uuid
|
||||
:block-uuid block-uuid}
|
||||
(string? session-id) (assoc :session-id session-id))))))
|
||||
vec)]
|
||||
(if-not (and (string? base)
|
||||
(string? (blank->nil planning-session-id))
|
||||
(seq bindings))
|
||||
(p/resolved nil)
|
||||
(db-sync/fetch-json (str base "/planning/sessions/" planning-session-id "/tasks/sync")
|
||||
{:method "POST"
|
||||
:headers {"content-type" "application/json"}
|
||||
:body (js/JSON.stringify (clj->js {:tasks bindings}))}
|
||||
{:response-schema :planning.sessions/get}))))
|
||||
|
||||
(defn <upsert-planner-tasks!
|
||||
([goal-block-uuid tasks]
|
||||
(<upsert-planner-tasks! goal-block-uuid tasks nil))
|
||||
([goal-block-uuid tasks opts]
|
||||
(if-let [goal-block (db/entity [:block/uuid goal-block-uuid])]
|
||||
(let [context (planner-goal-context goal-block opts)
|
||||
planning-session-id (blank->nil (:planning-session-id opts))]
|
||||
(p/loop [remaining (seq tasks)
|
||||
acc []
|
||||
anchor-block-uuid nil]
|
||||
(if-let [task (first remaining)]
|
||||
(p/let [result (<upsert-planner-task! goal-block anchor-block-uuid task context)]
|
||||
(p/recur (next remaining)
|
||||
(cond-> acc result (conj (merge task result)))
|
||||
(or (:block-uuid result)
|
||||
anchor-block-uuid)))
|
||||
(p/let [_ (<sync-planning-task-bindings! planning-session-id acc)]
|
||||
(mapv (fn [task]
|
||||
(select-keys task [:task-uuid :block-uuid :session-id]))
|
||||
acc)))))
|
||||
(p/resolved []))))
|
||||
|
||||
(def ^:private stream-reconnect-delay-ms 1500)
|
||||
|
||||
(defn- session-key [block-uuid]
|
||||
@@ -850,150 +454,6 @@
|
||||
(when (and session-id (not= current-session-id session-id))
|
||||
(property-handler/set-block-property! block-uuid task-session-id-property session-id)))))
|
||||
|
||||
(def ^:private planning-started-task-statuses
|
||||
#{"Doing" "Done" "In Review" "Canceled"})
|
||||
|
||||
(defn- planning-task-started?
|
||||
[task]
|
||||
(or (string? (blank->nil (:session-id task)))
|
||||
(contains? planning-started-task-statuses
|
||||
(some-> (:status task) str string/trim not-empty))))
|
||||
|
||||
(defn <fetch-planning-session!
|
||||
[block-uuid planning-session-id]
|
||||
(let [base (db-sync/http-base)
|
||||
planning-session-id (blank->nil planning-session-id)]
|
||||
(if-not (and (string? base) (string? planning-session-id))
|
||||
(p/resolved nil)
|
||||
(p/let [_ (js/Promise. user-handler/task--ensure-id&access-token)
|
||||
resp (db-sync/fetch-json (str base "/planning/sessions/" planning-session-id)
|
||||
{:method "GET"}
|
||||
{:response-schema :planning.sessions/get})]
|
||||
(apply-planning-session! block-uuid resp)))))
|
||||
|
||||
(defn <set-planning-approval!
|
||||
([block-uuid approval-status]
|
||||
(<set-planning-approval! block-uuid approval-status nil))
|
||||
([block-uuid approval-status opts]
|
||||
(let [base (db-sync/http-base)
|
||||
planning-session-id (or (some-> (session-state block-uuid) :planning-session-id blank->nil)
|
||||
(some-> (session-state block-uuid) :session-id blank->nil))
|
||||
approval-status (some-> approval-status str string/trim string/lower-case not-empty)
|
||||
approval-comment (some-> (:comment opts) blank->nil)]
|
||||
(if-not (and (string? base)
|
||||
(string? planning-session-id)
|
||||
(contains? #{"pending" "approved" "rejected"} approval-status))
|
||||
(p/resolved nil)
|
||||
(p/let [_ (js/Promise. user-handler/task--ensure-id&access-token)
|
||||
resp (db-sync/fetch-json (str base "/planning/sessions/" planning-session-id "/approval")
|
||||
{:method "POST"
|
||||
:headers {"content-type" "application/json"}
|
||||
:body (js/JSON.stringify
|
||||
(clj->js (cond-> {:approval {:decision approval-status}}
|
||||
(string? approval-comment) (assoc-in [:approval :comment] approval-comment))))}
|
||||
{:response-schema :planning.sessions/get})]
|
||||
(apply-planning-session! block-uuid resp))))))
|
||||
|
||||
(defn <replan-planning-session!
|
||||
([block-uuid]
|
||||
(<replan-planning-session! block-uuid nil))
|
||||
([block-uuid opts]
|
||||
(let [base (db-sync/http-base)
|
||||
planning-session-id (or (some-> (session-state block-uuid) :planning-session-id blank->nil)
|
||||
(some-> (session-state block-uuid) :session-id blank->nil))
|
||||
replan-note (some-> (:replan-note opts) blank->nil)]
|
||||
(if-not (and (string? base)
|
||||
(string? planning-session-id))
|
||||
(p/resolved nil)
|
||||
(p/let [_ (js/Promise. user-handler/task--ensure-id&access-token)
|
||||
resp (db-sync/fetch-json (str base "/planning/sessions/" planning-session-id "/replan")
|
||||
{:method "POST"
|
||||
:headers {"content-type" "application/json"}
|
||||
:body (js/JSON.stringify
|
||||
(clj->js (cond-> {}
|
||||
(string? replan-note) (assoc :replan-note replan-note))))}
|
||||
{:response-schema :planning.sessions/get})]
|
||||
(apply-planning-session! block-uuid resp))))))
|
||||
|
||||
(defn- <start-planned-task!
|
||||
[goal-block-uuid task]
|
||||
(let [goal-block (db/entity [:block/uuid goal-block-uuid])
|
||||
parent-session-id (or (some-> goal-block task-session-id blank->nil)
|
||||
(some-> goal-block-uuid str blank->nil))
|
||||
block-uuid (:block-uuid task)
|
||||
block (when block-uuid
|
||||
(db/entity [:block/uuid block-uuid]))
|
||||
existing-session-id (or (blank->nil (:session-id task))
|
||||
(some-> goal-block task-session-id blank->nil)
|
||||
(some-> block task-session-id blank->nil))]
|
||||
(cond
|
||||
(not block)
|
||||
(p/resolved nil)
|
||||
|
||||
(or existing-session-id
|
||||
(task-session-created? block)
|
||||
(planning-task-started? task))
|
||||
(let [session-id (or existing-session-id
|
||||
(task-session-id block))]
|
||||
(when (string? session-id)
|
||||
(when goal-block-uuid
|
||||
(maybe-store-task-session-id! goal-block-uuid session-id))
|
||||
(maybe-store-task-session-id! block-uuid session-id))
|
||||
(p/resolved (cond-> (select-keys task [:task-uuid :block-uuid])
|
||||
(string? session-id) (assoc :session-id session-id))))
|
||||
|
||||
:else
|
||||
(p/let [resp (<start-session! block {:session-id parent-session-id})
|
||||
session-id (or (some-> resp :session-id blank->nil)
|
||||
(some-> (db/entity [:block/uuid goal-block-uuid])
|
||||
task-session-id
|
||||
blank->nil)
|
||||
(some-> (db/entity [:block/uuid block-uuid])
|
||||
task-session-id
|
||||
blank->nil))]
|
||||
(when (string? session-id)
|
||||
(when goal-block-uuid
|
||||
(maybe-store-task-session-id! goal-block-uuid session-id))
|
||||
(maybe-store-task-session-id! block-uuid session-id))
|
||||
(cond-> (select-keys task [:task-uuid :block-uuid])
|
||||
(string? session-id) (assoc :session-id session-id))))))
|
||||
|
||||
(defn <execute-planned-tasks!
|
||||
([goal-block-uuid tasks]
|
||||
(<execute-planned-tasks! goal-block-uuid tasks nil))
|
||||
([goal-block-uuid tasks opts]
|
||||
(let [planning-session-id (blank->nil (:planning-session-id opts))
|
||||
selected-task-uuids (set (keep blank->nil (:selected-task-uuids opts)))]
|
||||
(p/let [persisted-tasks (<upsert-planner-tasks! goal-block-uuid
|
||||
tasks
|
||||
{:planning-session-id planning-session-id})
|
||||
persisted-by-task-uuid (into {}
|
||||
(keep (fn [task]
|
||||
(when-let [task-uuid (blank->nil (:task-uuid task))]
|
||||
[task-uuid task])))
|
||||
persisted-tasks)
|
||||
selected-tasks (->> tasks
|
||||
(map (fn [task]
|
||||
(merge task
|
||||
(get persisted-by-task-uuid
|
||||
(blank->nil (:task-uuid task))))))
|
||||
(filter (fn [task]
|
||||
(or (empty? selected-task-uuids)
|
||||
(contains? selected-task-uuids
|
||||
(blank->nil (:task-uuid task))))))
|
||||
vec)
|
||||
results (p/loop [remaining selected-tasks
|
||||
acc []]
|
||||
(if-let [task (first remaining)]
|
||||
(p/let [result (<start-planned-task! goal-block-uuid task)]
|
||||
(p/recur (next remaining)
|
||||
(cond-> acc result (conj result))))
|
||||
acc))
|
||||
sync-response (<sync-planning-task-bindings! planning-session-id results)]
|
||||
(when (map? sync-response)
|
||||
(apply-planning-session! goal-block-uuid sync-response))
|
||||
results))))
|
||||
|
||||
(defn- maybe-store-task-sandbox-checkpoint!
|
||||
[block-uuid checkpoint]
|
||||
(when-let [block (db/entity [:block/uuid block-uuid])]
|
||||
@@ -1013,34 +473,16 @@
|
||||
(checkpoint->property-value checkpoint))))))))
|
||||
|
||||
(defn- update-session!
|
||||
([block-uuid f]
|
||||
(update-session! block-uuid f nil))
|
||||
([block-uuid f bucket-key]
|
||||
(let [bucket-key (or bucket-key :agent/sessions)]
|
||||
(state/update-state! bucket-key
|
||||
(fn [sessions]
|
||||
(let [key (session-key block-uuid)
|
||||
session (get sessions key {})]
|
||||
(assoc sessions key (f session))))))))
|
||||
|
||||
(defn- session-bucket-key
|
||||
[block-uuid data]
|
||||
(let [session-kind (some-> (:session-kind data)
|
||||
blank->nil
|
||||
string/lower-case)
|
||||
planning-sessions (state/sub :agent/planning-sessions)
|
||||
key (session-key block-uuid)]
|
||||
(cond
|
||||
(= "planning" session-kind) :agent/planning-sessions
|
||||
(= "session" session-kind) :agent/sessions
|
||||
(contains? planning-sessions key) :agent/planning-sessions
|
||||
:else :agent/sessions)))
|
||||
[block-uuid f]
|
||||
(state/update-state! :agent/sessions
|
||||
(fn [sessions]
|
||||
(let [key (session-key block-uuid)
|
||||
session (get sessions key {})]
|
||||
(assoc sessions key (f session))))))
|
||||
|
||||
(defn- update-session-state!
|
||||
[block-uuid data]
|
||||
(update-session! block-uuid
|
||||
#(merge % data)
|
||||
(session-bucket-key block-uuid data)))
|
||||
(update-session! block-uuid #(merge % data)))
|
||||
|
||||
(defn- event->status [event]
|
||||
(case (:type event)
|
||||
@@ -1114,9 +556,7 @@
|
||||
:kind "user"}))
|
||||
|
||||
(defn- session-state [block-uuid]
|
||||
(let [key (session-key block-uuid)]
|
||||
(or (get (state/sub :agent/planning-sessions) key)
|
||||
(get (state/sub :agent/sessions) key))))
|
||||
(get (state/sub :agent/sessions) (session-key block-uuid)))
|
||||
|
||||
(defn <fetch-events!
|
||||
[block]
|
||||
@@ -1278,21 +718,11 @@
|
||||
(<connect-session-stream! block-uuid stream-url))))
|
||||
stream-reconnect-delay-ms))
|
||||
|
||||
(defn- planning-session?
|
||||
[session]
|
||||
(let [session-kind (some-> (:session-kind session)
|
||||
str
|
||||
string/trim
|
||||
string/lower-case)]
|
||||
(or (= "planning" session-kind)
|
||||
(string? (blank->nil (:planning-chat-path session))))))
|
||||
|
||||
(defn <ensure-session!
|
||||
[block]
|
||||
(let [block-uuid (:block/uuid block)
|
||||
base (db-sync/http-base)
|
||||
session (session-state block-uuid)
|
||||
planning? (planning-session? session)
|
||||
session-id (or (:session-id session)
|
||||
(task-session-id block)
|
||||
(some-> block-uuid str))]
|
||||
@@ -1302,81 +732,47 @@
|
||||
(p/resolved nil)
|
||||
|
||||
(:session-id session)
|
||||
(if planning?
|
||||
(do
|
||||
(when (and (not (:planning-loaded? session))
|
||||
(not (:planning-loading? session)))
|
||||
(update-session-state! block-uuid {:planning-loading? true})
|
||||
(-> (db-sync/fetch-json (str base "/planning/sessions/" session-id)
|
||||
{:method "GET"}
|
||||
{:response-schema :planning.sessions/get})
|
||||
(p/then (fn [resp]
|
||||
(apply-planning-session! block-uuid resp)))
|
||||
(p/catch (fn [error]
|
||||
(update-session-state! block-uuid {:planning-loading? false})
|
||||
(let [status (:status (ex-data error))]
|
||||
(when-not (= status 404)
|
||||
(log/error :agent/ensure-planning-session-failed error)))
|
||||
nil))))
|
||||
(p/resolved session))
|
||||
(do
|
||||
(maybe-store-task-session-id! block-uuid (:session-id session))
|
||||
(when-not (:streaming? session)
|
||||
(-> (<fetch-events! block)
|
||||
(p/then (fn [_]
|
||||
(<connect-session-stream! block-uuid (or (:stream-url (session-state block-uuid))
|
||||
(session-stream-url base session-id)))))))
|
||||
(p/resolved session)))
|
||||
(do
|
||||
(maybe-store-task-session-id! block-uuid (:session-id session))
|
||||
(when-not (:streaming? session)
|
||||
(-> (<fetch-events! block)
|
||||
(p/then (fn [_]
|
||||
(<connect-session-stream! block-uuid (or (:stream-url (session-state block-uuid))
|
||||
(session-stream-url base session-id)))))))
|
||||
(p/resolved session))
|
||||
|
||||
:else
|
||||
(if planning?
|
||||
(p/resolved nil)
|
||||
(do
|
||||
(update-session-state! block-uuid {:loading? true})
|
||||
(-> (p/let [resp (db-sync/fetch-json (str base "/sessions/" session-id)
|
||||
{:method "GET"}
|
||||
{:response-schema :sessions/get})
|
||||
session-id' (or (:session-id resp) session-id)
|
||||
stream-url (session-stream-url base session-id')]
|
||||
(update-session-state! block-uuid {:session-id session-id'
|
||||
:status (:status resp)
|
||||
:runtime-provider (:runtime-provider resp)
|
||||
:terminal-enabled (true? (:terminal-enabled resp))
|
||||
:stream-url stream-url
|
||||
:loading? false})
|
||||
(maybe-store-task-session-id! block-uuid session-id')
|
||||
(maybe-update-task-status! block-uuid (:status resp))
|
||||
(<fetch-events! block)
|
||||
(<connect-session-stream! block-uuid (or (:stream-url (session-state block-uuid))
|
||||
stream-url))
|
||||
resp)
|
||||
(p/catch (fn [error]
|
||||
(update-session-state! block-uuid {:loading? false})
|
||||
(let [status (:status (ex-data error))]
|
||||
(when-not (= status 404)
|
||||
(log/error :agent/ensure-session-failed error)))
|
||||
nil)))))))))
|
||||
|
||||
(defn- <ensure-auth!
|
||||
[]
|
||||
(js/Promise. user-handler/task--ensure-id&access-token))
|
||||
|
||||
(defn- planning-create-path?
|
||||
[create-path]
|
||||
(= "/planning/sessions" (some-> create-path str string/trim)))
|
||||
(do
|
||||
(update-session-state! block-uuid {:loading? true})
|
||||
(-> (p/let [resp (db-sync/fetch-json (str base "/sessions/" session-id)
|
||||
{:method "GET"}
|
||||
{:response-schema :sessions/get})
|
||||
session-id' (or (:session-id resp) session-id)
|
||||
stream-url (session-stream-url base session-id')]
|
||||
(update-session-state! block-uuid {:session-id session-id'
|
||||
:status (:status resp)
|
||||
:runtime-provider (:runtime-provider resp)
|
||||
:terminal-enabled (true? (:terminal-enabled resp))
|
||||
:stream-url stream-url
|
||||
:loading? false})
|
||||
(maybe-store-task-session-id! block-uuid session-id')
|
||||
(maybe-update-task-status! block-uuid (:status resp))
|
||||
(<fetch-events! block)
|
||||
(<connect-session-stream! block-uuid (or (:stream-url (session-state block-uuid))
|
||||
stream-url))
|
||||
resp)
|
||||
(p/catch (fn [error]
|
||||
(update-session-state! block-uuid {:loading? false})
|
||||
(let [status (:status (ex-data error))]
|
||||
(when-not (= status 404)
|
||||
(log/error :agent/ensure-session-failed error)))
|
||||
nil))))))))
|
||||
|
||||
(defn <start-session!
|
||||
([block]
|
||||
(<start-session! block nil))
|
||||
([block opts]
|
||||
(<start-session! block opts "/sessions"))
|
||||
([block opts create-path]
|
||||
(let [base (db-sync/http-base)
|
||||
opts (or opts {})
|
||||
planning? (planning-create-path? create-path)
|
||||
response-schema (if planning?
|
||||
:planning.sessions/create
|
||||
:sessions/create)]
|
||||
(let [base (db-sync/http-base)]
|
||||
(cond
|
||||
(not base)
|
||||
(do
|
||||
@@ -1389,78 +785,39 @@
|
||||
(p/resolved nil))
|
||||
|
||||
:else
|
||||
(p/let [_ (<ensure-auth!)
|
||||
(p/let [_ (js/Promise. user-handler/task--ensure-id&access-token)
|
||||
raw-body (build-session-body block opts)
|
||||
body (coerce-http-request :sessions/create raw-body)]
|
||||
(if (nil? body)
|
||||
(do
|
||||
(notification/show! "Invalid agent session payload." :error false)
|
||||
nil)
|
||||
(-> (p/let [resp (db-sync/fetch-json (str base create-path)
|
||||
(-> (p/let [resp (db-sync/fetch-json (str base "/sessions")
|
||||
{:method "POST"
|
||||
:headers {"content-type" "application/json"}
|
||||
:body (js/JSON.stringify (clj->js body))}
|
||||
{:response-schema response-schema})
|
||||
session-id (or (:session-id resp)
|
||||
(:planning-session-id resp))
|
||||
{:response-schema :sessions/create})
|
||||
session-id (:session-id resp)
|
||||
status (:status resp)
|
||||
stream-url (when-not planning?
|
||||
(:stream-url resp))
|
||||
planning-chat-path (when planning?
|
||||
(or (blank->nil (:chat-path resp))
|
||||
(when (string? session-id)
|
||||
(str "/planning/chat/" session-id))))
|
||||
stream-url (:stream-url resp)
|
||||
block-uuid (:block/uuid block)
|
||||
_ (when (and (not planning?)
|
||||
(string? session-id))
|
||||
(when-let [raw-message (message-body (:content raw-body))]
|
||||
(let [coerced (coerce-http-request :sessions/message raw-message)
|
||||
msg-body (if (map? coerced) coerced raw-message)]
|
||||
(db-sync/fetch-json (str base "/sessions/" session-id "/messages")
|
||||
{:method "POST"
|
||||
:headers {"content-type" "application/json"}
|
||||
:body (js/JSON.stringify (clj->js msg-body))}
|
||||
{:response-schema :sessions/message}))))]
|
||||
_ (when-let [raw-message (message-body (:content raw-body))]
|
||||
(let [coerced (coerce-http-request :sessions/message raw-message)
|
||||
msg-body (if (map? coerced) coerced raw-message)]
|
||||
(db-sync/fetch-json (str base "/sessions/" session-id "/messages")
|
||||
{:method "POST"
|
||||
:headers {"content-type" "application/json"}
|
||||
:body (js/JSON.stringify (clj->js msg-body))}
|
||||
{:response-schema :sessions/message})))]
|
||||
(notification/clear! github-install-required-notification-uid)
|
||||
(if planning?
|
||||
(update-session-state! block-uuid {:session-id session-id
|
||||
:session-kind "planning"
|
||||
:planning-session-id session-id
|
||||
:planning-chat-path planning-chat-path
|
||||
:workflow-id (:workflow-id resp)
|
||||
:status status
|
||||
:approval-status (:approval-status resp)
|
||||
:require-approval (true? (:require-approval resp))
|
||||
:auto-dispatch (if (boolean? (:auto-dispatch resp))
|
||||
(:auto-dispatch resp)
|
||||
true)
|
||||
:auto-replan (true? (:auto-replan resp))
|
||||
:replan-delay-sec (:replan-delay-sec resp)
|
||||
:runtime-provider nil
|
||||
:terminal-enabled false
|
||||
:stream-url nil
|
||||
:streaming? false
|
||||
:stream-controller nil
|
||||
:plan nil
|
||||
:dispatch-sessions nil
|
||||
:scheduled-actions nil
|
||||
:planning-messages nil
|
||||
:planning-loaded? false
|
||||
:planning-loading? false
|
||||
:started-at (util/time-ms)})
|
||||
(update-session-state! block-uuid {:session-id session-id
|
||||
:session-kind "session"
|
||||
:planning-session-id nil
|
||||
:planning-chat-path nil
|
||||
:status status
|
||||
:runtime-provider (:runtime-provider resp)
|
||||
:terminal-enabled (true? (:terminal-enabled resp))
|
||||
:stream-url stream-url
|
||||
:started-at (util/time-ms)}))
|
||||
(when (and (not planning?)
|
||||
(string? session-id))
|
||||
(maybe-store-task-session-id! block-uuid session-id)
|
||||
(<connect-session-stream! block-uuid stream-url))
|
||||
(update-session-state! block-uuid {:session-id session-id
|
||||
:status status
|
||||
:runtime-provider (:runtime-provider resp)
|
||||
:terminal-enabled (true? (:terminal-enabled resp))
|
||||
:stream-url stream-url
|
||||
:started-at (util/time-ms)})
|
||||
(maybe-store-task-session-id! block-uuid session-id)
|
||||
(<connect-session-stream! block-uuid stream-url)
|
||||
resp)
|
||||
(p/catch (fn [error]
|
||||
(cond
|
||||
@@ -1473,34 +830,12 @@
|
||||
(show-github-install-required-notification!
|
||||
error
|
||||
(fn []
|
||||
(-> (<start-session! block opts create-path)
|
||||
(-> (<start-session! block opts)
|
||||
(p/catch (fn [_] nil)))))
|
||||
:else
|
||||
(notification/show! (start-session-error-message error) :error false))
|
||||
nil)))))))))
|
||||
|
||||
(defn <start-planning-session!
|
||||
([block]
|
||||
(<start-planning-session! block nil))
|
||||
([block opts]
|
||||
(if-not (planning-enabled?)
|
||||
(do
|
||||
(notification/show! "Planning is not enabled for this build/account." :warning false)
|
||||
(p/resolved nil))
|
||||
(let [opts (cond-> (or opts {})
|
||||
(nil? (:agent/permission-mode opts))
|
||||
(assoc :agent/permission-mode "read-only"))]
|
||||
(<start-session! block opts "/planning/sessions")))))
|
||||
|
||||
(defn <start-auto-session!
|
||||
([block]
|
||||
(<start-auto-session! block nil))
|
||||
([block opts]
|
||||
(let [{:keys [planning?]} (session-start-strategy block)]
|
||||
(if planning?
|
||||
(<start-planning-session! block opts)
|
||||
(<start-session! block opts)))))
|
||||
|
||||
(defn- publish-request-body
|
||||
[{:keys [title body commit-message head-branch base-branch create-pr? force?]}]
|
||||
(cond-> {:create-pr (if (nil? create-pr?) true (true? create-pr?))
|
||||
|
||||
@@ -126,21 +126,6 @@
|
||||
[base session-id]
|
||||
(str base "/sessions/" session-id "/messages"))
|
||||
|
||||
(defn- endpoint-url
|
||||
[base endpoint]
|
||||
(let [endpoint (non-empty-str endpoint)]
|
||||
(cond
|
||||
(and (string? endpoint)
|
||||
(or (string/starts-with? endpoint "http://")
|
||||
(string/starts-with? endpoint "https://")))
|
||||
endpoint
|
||||
|
||||
(and (string? base) (string? endpoint))
|
||||
(str base endpoint)
|
||||
|
||||
:else
|
||||
nil)))
|
||||
|
||||
(defn- runtime-error-message
|
||||
[event]
|
||||
(let [data (if (map? (:data event)) (:data event) {})]
|
||||
@@ -588,14 +573,7 @@
|
||||
(.finally (fn [] nil))))))
|
||||
|
||||
(defn- send-messages!
|
||||
[{:keys [base
|
||||
session-id
|
||||
fetch-fn
|
||||
now-fn
|
||||
idle-timeout-ms
|
||||
open-stream?
|
||||
message-endpoint
|
||||
stream-endpoint]} opts]
|
||||
[{:keys [base session-id fetch-fn now-fn idle-timeout-ms open-stream?]} opts]
|
||||
(let [message (last-user-message-text (aget opts "messages"))]
|
||||
(if-not (and (string? base) (string? session-id))
|
||||
(js/Promise.reject
|
||||
@@ -607,10 +585,8 @@
|
||||
start-ts (now-fn)
|
||||
abort-signal (aget opts "abortSignal")
|
||||
headers (auth-headers)
|
||||
post-url (or (endpoint-url base message-endpoint)
|
||||
(messages-url base session-id))
|
||||
stream-url' (or (endpoint-url base stream-endpoint)
|
||||
(stream-url base session-id))]
|
||||
post-url (messages-url base session-id)
|
||||
stream-endpoint (stream-url base session-id)]
|
||||
(-> (p/let [post-resp (fetch-fn post-url
|
||||
#js {:method "POST"
|
||||
:headers headers
|
||||
@@ -622,45 +598,34 @@
|
||||
(throw (js/Error. (str "send message failed: " (.-status post-resp)))))]
|
||||
(if (false? open-stream?)
|
||||
(chunks->readable-stream [{:type "finish"}])
|
||||
(if-not (string? stream-url')
|
||||
(chunks->readable-stream [{:type "finish"}])
|
||||
(p/let [stream-resp (fetch-fn stream-url'
|
||||
#js {:method "GET"
|
||||
:headers headers
|
||||
:signal abort-signal})
|
||||
_ (when-not (.-ok stream-resp)
|
||||
(throw (js/Error. (str "open stream failed: " (.-status stream-resp)))))
|
||||
_ (when-not (.-body stream-resp)
|
||||
(throw (js/Error. "stream response has no body")))]
|
||||
(let [ts (js/TransformStream.)
|
||||
writer (.getWriter (.-writable ts))]
|
||||
(start-stream-consumer! {:response stream-resp
|
||||
:writer writer
|
||||
:start-ts start-ts
|
||||
:idle-timeout-ms (or idle-timeout-ms default-idle-timeout-ms)
|
||||
:abort-signal abort-signal})
|
||||
(.-readable ts))))))
|
||||
(p/let [stream-resp (fetch-fn stream-endpoint
|
||||
#js {:method "GET"
|
||||
:headers headers
|
||||
:signal abort-signal})
|
||||
_ (when-not (.-ok stream-resp)
|
||||
(throw (js/Error. (str "open stream failed: " (.-status stream-resp)))))
|
||||
_ (when-not (.-body stream-resp)
|
||||
(throw (js/Error. "stream response has no body")))]
|
||||
(let [ts (js/TransformStream.)
|
||||
writer (.getWriter (.-writable ts))]
|
||||
(start-stream-consumer! {:response stream-resp
|
||||
:writer writer
|
||||
:start-ts start-ts
|
||||
:idle-timeout-ms (or idle-timeout-ms default-idle-timeout-ms)
|
||||
:abort-signal abort-signal})
|
||||
(.-readable ts)))))
|
||||
(.catch (fn [error]
|
||||
(js/Promise.reject error)))))))))
|
||||
|
||||
(defn make-transport
|
||||
[{:keys [base
|
||||
session-id
|
||||
fetch-fn
|
||||
now-fn
|
||||
idle-timeout-ms
|
||||
open-stream?
|
||||
message-endpoint
|
||||
stream-endpoint]}]
|
||||
[{:keys [base session-id fetch-fn now-fn idle-timeout-ms open-stream?]}]
|
||||
#js {:sendMessages (fn [opts]
|
||||
(send-messages! {:base base
|
||||
:session-id session-id
|
||||
:fetch-fn fetch-fn
|
||||
:now-fn now-fn
|
||||
:idle-timeout-ms idle-timeout-ms
|
||||
:open-stream? open-stream?
|
||||
:message-endpoint message-endpoint
|
||||
:stream-endpoint stream-endpoint}
|
||||
:open-stream? open-stream?}
|
||||
opts))
|
||||
:reconnectToStream (fn [_opts]
|
||||
(js/Promise.resolve nil))})
|
||||
|
||||
@@ -293,7 +293,6 @@
|
||||
:rtc/asset-upload-download-progress (atom {})
|
||||
:rtc/users-info (atom {})
|
||||
:agent/sessions (atom {})
|
||||
:agent/planning-sessions (atom {})
|
||||
|
||||
:user/info {:UserGroups (storage/get :user-groups)}
|
||||
:encryption/graph-parsing? false
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
(ns frontend.handler.agent-test
|
||||
(:require [clojure.string :as string]
|
||||
[clojure.test :refer [deftest is testing use-fixtures]]
|
||||
[frontend.db :as db]
|
||||
[frontend.handler.agent :as agent-handler]
|
||||
[frontend.handler.db-based.property :as db-property-handler]
|
||||
[frontend.handler.property.util :as pu]
|
||||
[frontend.test.helper :as test-helper :include-macros true :refer [deftest-async]]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(use-fixtures :each {:before test-helper/start-test-db!
|
||||
:after (fn [] (test-helper/destroy-test-db!))})
|
||||
|
||||
(defn- setup-goal-context!
|
||||
[]
|
||||
(test-helper/load-test-files [{:page {:block/title "Goal Page"}
|
||||
:blocks [{:block/title "Goal"}]}
|
||||
{:page {:block/title "Project Alpha"}}
|
||||
{:page {:block/title "Codex"}}])
|
||||
(let [goal (test-helper/find-block-by-content "Goal")
|
||||
project-page (db/get-page "Project Alpha")
|
||||
agent-page (db/get-page "Codex")]
|
||||
(db-property-handler/set-block-property! (:db/id project-page) :block/tags :logseq.class/Project)
|
||||
(db-property-handler/set-block-property! (:db/id project-page) :logseq.property/git-repo "https://github.com/logseq/logseq")
|
||||
(db-property-handler/set-block-property! (:db/id agent-page) :block/tags :logseq.class/Agent)
|
||||
(db-property-handler/set-block-property! (:db/id goal) :logseq.property/project "Project Alpha")
|
||||
(db-property-handler/set-block-property! (:db/id goal) :logseq.property/agent "Codex")
|
||||
{:goal goal
|
||||
:project-page project-page
|
||||
:agent-page agent-page}))
|
||||
|
||||
(deftest-async persist-planner-tasks-creates-runnable-task-blocks-test
|
||||
(p/let [{:keys [goal]} (setup-goal-context!)
|
||||
tasks (agent-handler/<upsert-planner-tasks!
|
||||
(:block/uuid goal)
|
||||
[{:title "Implement planner tasks"
|
||||
:description "Create Logseq tasks from planner output"}])
|
||||
task-uuid (:block-uuid (first tasks))
|
||||
task-block (db/entity [:block/uuid task-uuid])]
|
||||
(is (= 1 (count tasks)))
|
||||
(is (some #(= :logseq.class/Task (:db/ident %)) (:block/tags task-block)))
|
||||
(is (= "Todo" (pu/get-block-property-value task-block :logseq.property/status)))
|
||||
(is (= "Project Alpha" (some-> (:logseq.property/project task-block) :block/title)))
|
||||
(is (= "Codex" (some-> (:logseq.property/agent task-block) :block/title)))
|
||||
(is (some? (agent-handler/build-session-body task-block)))))
|
||||
|
||||
(deftest-async upsert-planner-tasks-reconciles-existing-task-by-block-uuid-test
|
||||
(p/let [{:keys [goal]} (setup-goal-context!)
|
||||
created (agent-handler/<upsert-planner-tasks!
|
||||
(:block/uuid goal)
|
||||
[{:title "Initial title"
|
||||
:description "Initial description"}])
|
||||
block-uuid (:block-uuid (first created))
|
||||
_ (agent-handler/<upsert-planner-tasks!
|
||||
(:block/uuid goal)
|
||||
[{:block-uuid block-uuid
|
||||
:title "Updated title"
|
||||
:description "Updated description"}])
|
||||
updated (db/entity [:block/uuid block-uuid])]
|
||||
(is (= "Updated title\nUpdated description" (:block/title updated)))
|
||||
(is (= "Todo" (pu/get-block-property-value updated :logseq.property/status)))))
|
||||
|
||||
(deftest-async upsert-planner-tasks-does-not-overwrite-task-after-session-start-test
|
||||
(p/let [{:keys [goal]} (setup-goal-context!)
|
||||
created (agent-handler/<upsert-planner-tasks!
|
||||
(:block/uuid goal)
|
||||
[{:title "Locked task"
|
||||
:description "Original"}])
|
||||
block-uuid (:block-uuid (first created))
|
||||
_ (db-property-handler/set-block-property! block-uuid :logseq.property/agent-session-id "sess-1")
|
||||
_ (agent-handler/<upsert-planner-tasks!
|
||||
(:block/uuid goal)
|
||||
[{:block-uuid block-uuid
|
||||
:title "Changed title"
|
||||
:description "Changed description"}])
|
||||
unchanged (db/entity [:block/uuid block-uuid])]
|
||||
(is (= "Locked task\nOriginal" (:block/title unchanged)))
|
||||
(is (= "sess-1" (agent-handler/task-session-id unchanged)))))
|
||||
|
||||
(deftest-async build-session-body-includes-explicit-agent-permission-mode-test
|
||||
(p/let [{:keys [goal]} (setup-goal-context!)
|
||||
created (agent-handler/<upsert-planner-tasks!
|
||||
(:block/uuid goal)
|
||||
[{:title "Planning task"
|
||||
:description "Plan safely"}])
|
||||
task-block (db/entity [:block/uuid (:block-uuid (first created))])
|
||||
body (agent-handler/build-session-body task-block {:agent/permission-mode "read-only"})]
|
||||
(is (= "codex" (some-> body :agent :provider string/lower-case)))
|
||||
(is (= "read-only" (get-in body [:agent :permission-mode])))))
|
||||
|
||||
(deftest planner-tasks-from-text-parses-json-object-and-code-fence-test
|
||||
(testing "json object with tasks"
|
||||
(is (= [{:title "Task A"
|
||||
:description "Desc A"}
|
||||
{:title "Task B"
|
||||
:content "Explicit content"}]
|
||||
(agent-handler/planner-tasks-from-text
|
||||
"{\"tasks\":[{\"title\":\"Task A\",\"description\":\"Desc A\"},{\"title\":\"Task B\",\"content\":\"Explicit content\"}]}"))))
|
||||
(testing "fenced json array"
|
||||
(is (= [{:title "Task C"
|
||||
:description "Desc C"}]
|
||||
(agent-handler/planner-tasks-from-text
|
||||
"```json\n[{\"title\":\"Task C\",\"description\":\"Desc C\"}]\n```"))))
|
||||
(testing "prose with fenced json block"
|
||||
(is (= [{:title "Task D"
|
||||
:description "Desc D"}]
|
||||
(agent-handler/planner-tasks-from-text
|
||||
"Here is the plan.\n\n```json\n{\"tasks\":[{\"title\":\"Task D\",\"description\":\"Desc D\"}]}\n```\n\nReview it."))))
|
||||
(testing "invalid payload returns nil"
|
||||
(is (nil? (agent-handler/planner-tasks-from-text "not planner json")))))
|
||||
Reference in New Issue
Block a user