mirror of
https://github.com/logseq/logseq.git
synced 2026-05-25 05:04:24 +00:00
sandbox plan
This commit is contained in:
10
deps/db/src/logseq/db/frontend/property.cljs
vendored
10
deps/db/src/logseq/db/frontend/property.cljs
vendored
@@ -437,6 +437,16 @@
|
||||
:public? false
|
||||
:hide? true}
|
||||
:queryable? false}
|
||||
:logseq.property/agent-plan
|
||||
{:title "Agent plan"
|
||||
:schema {:type :default
|
||||
:public? true}
|
||||
:properties {:logseq.property/description "Stores the task plan markdown emitted at the start of an agent session."}}
|
||||
:logseq.property/post-review
|
||||
{:title "Post-review"
|
||||
:schema {:type :default
|
||||
:public? true}
|
||||
:properties {:logseq.property/description "Stores the post-review markdown emitted when an agent session finishes."}}
|
||||
:logseq.property/git-repo
|
||||
{:title "Git Repo"
|
||||
:schema {:type :url
|
||||
|
||||
2
deps/db/src/logseq/db/frontend/schema.cljs
vendored
2
deps/db/src/logseq/db/frontend/schema.cljs
vendored
@@ -30,7 +30,7 @@
|
||||
(map (juxt :major :minor)
|
||||
[(parse-schema-version x) (parse-schema-version y)])))
|
||||
|
||||
(def version (parse-schema-version "65.29"))
|
||||
(def version (parse-schema-version "65.30"))
|
||||
|
||||
(defn major-version
|
||||
"Return a number.
|
||||
|
||||
3
deps/workers/deps.edn
vendored
3
deps/workers/deps.edn
vendored
@@ -1,4 +1,4 @@
|
||||
{:paths ["src" "test" "../../resources"]
|
||||
{:paths ["src" "../../resources"]
|
||||
:deps
|
||||
{org.clojure/clojure {:mvn/version "1.11.1"}
|
||||
datascript/datascript {:git/url "https://github.com/logseq/datascript"
|
||||
@@ -19,6 +19,7 @@
|
||||
org.clojars.knubie/cljs-run-test {:mvn/version "1.0.1"}
|
||||
tortue/spy {:mvn/version "2.14.0"}}
|
||||
:main-opts ["-m" "shadow.cljs.devtools.cli"]}
|
||||
:test {:extra-paths ["test"]}
|
||||
:clj-kondo
|
||||
{:replace-deps {clj-kondo/clj-kondo {:mvn/version "2024.09.27"}}
|
||||
:main-opts ["-m" "clj-kondo.main"]}}}
|
||||
|
||||
@@ -24,3 +24,5 @@ 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-plan-first-execution-and-post-review.md`
|
||||
|
||||
81
deps/workers/docs/milestones/agents/26-m26-plan-first-execution-and-post-review.md
vendored
Normal file
81
deps/workers/docs/milestones/agents/26-m26-plan-first-execution-and-post-review.md
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
# M26: Plan-First Execution and Post-Review Persistence
|
||||
|
||||
Status: Implemented
|
||||
Target: Make every agent execution session start with a planning phase in the same session, persist the generated plan onto the task, create or update split subtasks in Logseq when the plan includes them, and persist a final post-review markdown artifact when the session completes.
|
||||
|
||||
## Goal
|
||||
Keep execution in a single agent session while forcing a plan-first workflow that writes structured planning and review artifacts back into Logseq.
|
||||
|
||||
## Why M26
|
||||
- Execution should not start from an unstructured prompt with no persisted plan.
|
||||
- Logseq needs a durable record of the agent's implementation plan, including any proposed split subtasks.
|
||||
- Completed sessions should leave behind a reusable post-review artifact instead of requiring users to reconstruct what changed from the raw chat stream.
|
||||
|
||||
## Implemented Flow
|
||||
1. Task session creation still starts a normal execution session.
|
||||
2. The initial task prompt now embeds an artifact protocol that requires:
|
||||
- a `<logseq-plan>...</logseq-plan>` artifact before code changes
|
||||
- continued implementation in the same session after the plan
|
||||
- a `<logseq-post-review>...</logseq-post-review>` artifact when the task is finished
|
||||
3. The frontend watches fetched and streamed session events.
|
||||
4. When a plan artifact appears, Logseq stores:
|
||||
- `Agent plan`
|
||||
- generated child tasks when `subtasks` are present
|
||||
5. When a post-review artifact appears, Logseq stores:
|
||||
- `Post-review`
|
||||
|
||||
## Current State
|
||||
- Plan/review prompt protocol and event persistence: `src/main/frontend/handler/agent.cljs`
|
||||
- Built-in task properties:
|
||||
- `deps/db/src/logseq/db/frontend/property.cljs`
|
||||
- Schema version bump:
|
||||
- `deps/db/src/logseq/db/frontend/schema.cljs`
|
||||
- Frontend migration wiring:
|
||||
- `src/main/frontend/worker/db/migrate.cljs`
|
||||
- Tests:
|
||||
- `src/test/frontend/handler/agent_test.cljs`
|
||||
- `src/test/frontend/worker/migrate_test.cljs`
|
||||
|
||||
## Final Decisions
|
||||
|
||||
### D1: Single Session
|
||||
- Planning and execution happen in the same agent session.
|
||||
- No separate planner session is introduced.
|
||||
|
||||
### D2: Logseq Artifacts
|
||||
- The persisted planning artifact is markdown stored on the task itself.
|
||||
- Suggested split work is stored as child tasks under the parent task.
|
||||
- The persisted review artifact is markdown stored on the task itself.
|
||||
|
||||
### D3: Artifact Contract
|
||||
- The planning artifact is emitted inside `<logseq-plan>`.
|
||||
- The completion artifact is emitted inside `<logseq-post-review>`.
|
||||
- Artifact payloads are JSON so the frontend can extract them deterministically from streamed agent output.
|
||||
|
||||
## Data Model
|
||||
- `:logseq.property/agent-plan`
|
||||
- `:logseq.property/post-review`
|
||||
|
||||
## Out of Scope
|
||||
- Server-side task graph mutation without a connected Logseq client.
|
||||
- A separate planner-only mode or planner session type.
|
||||
|
||||
## Validation
|
||||
- Added migration coverage for the new built-in properties.
|
||||
- Added frontend handler coverage for:
|
||||
- plan-first prompt construction
|
||||
- plan artifact persistence
|
||||
- post-review artifact persistence
|
||||
- Build verification completed with:
|
||||
- `clojure -M:test compile test-no-worker`
|
||||
|
||||
## Known Limitation
|
||||
- The repo's JS test runner currently fails before executing frontend tests in this environment because the root Node runtime cannot resolve the `e2b` module.
|
||||
- That issue is environmental and separate from the changed frontend/DB code.
|
||||
|
||||
## Exit Criteria
|
||||
1. Every execution prompt instructs the agent to emit a plan artifact before coding.
|
||||
2. Logseq persists the plan markdown to the task.
|
||||
3. Logseq creates or updates split subtasks when the plan includes them.
|
||||
4. Logseq persists a final post-review markdown artifact when the session completes.
|
||||
5. Planning and execution remain in the same session.
|
||||
6
deps/workers/package.json
vendored
6
deps/workers/package.json
vendored
@@ -13,9 +13,9 @@
|
||||
"migrate:local": "cd ./worker && wrangler d1 migrations apply DB --local",
|
||||
"migrate:staging": "cd ./worker && wrangler d1 migrations apply logseq-sync-graph-meta-staging --env staging --remote",
|
||||
"migrate:prod": "cd ./worker && wrangler d1 migrations apply logseq-sync-graphs-prod --env prod --remote",
|
||||
"test:node-adapter": "clojure -M:cljs compile db-sync-test && node worker/dist/worker-test.js",
|
||||
"test:worker-split": "clojure -M:cljs compile db-sync-worker-split-test && node worker/dist/worker-split-test.js",
|
||||
"test": "clojure -M:cljs compile db-sync-test && node worker/dist/worker-test.js",
|
||||
"test:node-adapter": "clojure -M:cljs:test compile db-sync-test && node worker/dist/worker-test.js",
|
||||
"test:worker-split": "clojure -M:cljs:test compile db-sync-worker-split-test && node worker/dist/worker-split-test.js",
|
||||
"test": "clojure -M:cljs:test compile db-sync-test && node worker/dist/worker-test.js",
|
||||
"clean": "rm -rf ./worker/dist/",
|
||||
"sentry:sourcemaps": "sentry-cli sourcemaps upload --release $SENTRY_RELEASE --rewrite --strip-prefix worker/dist/worker --url-prefix \"~/\" worker/dist/worker",
|
||||
"sentry:sourcemaps:agents": "sentry-cli sourcemaps upload --release $SENTRY_RELEASE --rewrite --strip-prefix worker/dist/agents --url-prefix \"~/\" worker/dist/agents",
|
||||
|
||||
236
deps/workers/src/logseq/agents/do.cljs
vendored
236
deps/workers/src/logseq/agents/do.cljs
vendored
@@ -24,6 +24,32 @@
|
||||
(defn- sse-bytes [event]
|
||||
(.encode (js/TextEncoder.) (sse-encode event)))
|
||||
|
||||
(defn- make-event
|
||||
[session-id type data ts]
|
||||
{:event-id (str (random-uuid))
|
||||
:session-id session-id
|
||||
:type type
|
||||
:ts (or ts (common/now-ms))
|
||||
:data data})
|
||||
|
||||
(defn- acp-live-message-event
|
||||
[session-id runtime-session-id turn text reasoning ts]
|
||||
(let [text (some-> text str string/trim not-empty)
|
||||
reasoning (some-> reasoning str string/trim not-empty)
|
||||
parts (cond-> []
|
||||
(string? reasoning) (conj {:type "reasoning" :text reasoning})
|
||||
(string? text) (conj {:type "text" :text text}))]
|
||||
(when (seq parts)
|
||||
{:event-id (str "live-" runtime-session-id "-turn-" turn)
|
||||
:session-id session-id
|
||||
:type "item.completed"
|
||||
:ts (or ts (common/now-ms))
|
||||
:data {:item_id (str runtime-session-id "-turn-" turn)
|
||||
:item {:item_id (str runtime-session-id "-turn-" turn)
|
||||
:kind "message"
|
||||
:role "assistant"
|
||||
:content parts}}})))
|
||||
|
||||
(defn- <storage-get [storage key]
|
||||
(p/let [value (.get storage key)]
|
||||
(when value (js->clj value :keywordize-keys true))))
|
||||
@@ -101,6 +127,52 @@
|
||||
(:task session)
|
||||
{}))
|
||||
|
||||
(defn- sanitize-agent-for-public
|
||||
[agent]
|
||||
(if (map? agent)
|
||||
(dissoc agent :api-token :auth-json :managed-auth)
|
||||
agent))
|
||||
|
||||
(defn- sanitize-task-for-public
|
||||
[task]
|
||||
(if (map? task)
|
||||
(update task :agent sanitize-agent-for-public)
|
||||
task))
|
||||
|
||||
(defn- sanitize-event-data-for-public
|
||||
[data]
|
||||
(if (map? data)
|
||||
(cond-> data
|
||||
(contains? data :agent) (update :agent sanitize-agent-for-public)
|
||||
(contains? data :task) (update :task sanitize-task-for-public))
|
||||
data))
|
||||
|
||||
(defn- sanitize-event-for-public
|
||||
[event]
|
||||
(if (map? event)
|
||||
(update event :data sanitize-event-data-for-public)
|
||||
event))
|
||||
|
||||
(defn- session-live-messages
|
||||
[session]
|
||||
(if (map? (:live-messages session))
|
||||
(:live-messages session)
|
||||
{}))
|
||||
|
||||
(defn- assoc-live-message
|
||||
[session runtime-session-id event]
|
||||
(assoc session :live-messages
|
||||
(cond-> (session-live-messages session)
|
||||
(and (string? runtime-session-id) (map? event))
|
||||
(assoc runtime-session-id event))))
|
||||
|
||||
(defn- dissoc-live-message
|
||||
[session runtime-session-id]
|
||||
(assoc session :live-messages
|
||||
(if (string? runtime-session-id)
|
||||
(dissoc (session-live-messages session) runtime-session-id)
|
||||
(session-live-messages session))))
|
||||
|
||||
(defn- runtime-snapshot-id
|
||||
[result]
|
||||
(some-> (:snapshot-id result) str string/trim not-empty))
|
||||
@@ -220,6 +292,93 @@
|
||||
(.catch (fn [_]
|
||||
(.delete streams key)))))))))
|
||||
|
||||
(defn- runtime-delta-event?
|
||||
[event-type]
|
||||
(and (string? event-type)
|
||||
(or (= "item.delta" event-type)
|
||||
(= "response.delta" event-type)
|
||||
(string/ends-with? event-type ".delta"))))
|
||||
|
||||
(defn- runtime-transient-event?
|
||||
[event-type]
|
||||
(contains? #{"item.started"
|
||||
"response.started"}
|
||||
event-type))
|
||||
|
||||
(defn- ensure-acp-turn-state!
|
||||
[^js self runtime-session-id]
|
||||
(let [store (or (.-acpTurnState self)
|
||||
(let [m (js/Map.)]
|
||||
(set! (.-acpTurnState self) m)
|
||||
m))
|
||||
existing (.get store runtime-session-id)]
|
||||
(if existing
|
||||
existing
|
||||
(let [state #js {:turn 1
|
||||
:text ""
|
||||
:reasoning ""}]
|
||||
(.set store runtime-session-id state)
|
||||
state))))
|
||||
|
||||
(defn- remember-acp-runtime-session-id!
|
||||
[^js self runtime-session-id]
|
||||
(when (string? runtime-session-id)
|
||||
(set! (.-lastAcpRuntimeSessionId self) runtime-session-id)))
|
||||
|
||||
(defn- last-acp-runtime-session-id
|
||||
[^js self]
|
||||
(some-> (.-lastAcpRuntimeSessionId self) str string/trim not-empty))
|
||||
|
||||
(defn- clear-acp-turn-state!
|
||||
[^js self runtime-session-id]
|
||||
(when-let [store (.-acpTurnState self)]
|
||||
(.delete store runtime-session-id))
|
||||
(when (= runtime-session-id (last-acp-runtime-session-id self))
|
||||
(set! (.-lastAcpRuntimeSessionId self) nil)))
|
||||
|
||||
(defn- append-acp-text!
|
||||
[^js self runtime-session-id field text]
|
||||
(when (and (string? runtime-session-id)
|
||||
(string? text)
|
||||
(contains? #{"text" "reasoning"} field))
|
||||
(let [state (ensure-acp-turn-state! self runtime-session-id)
|
||||
current (or (aget state field) "")]
|
||||
(aset state field (str current text)))))
|
||||
|
||||
(defn- acp-completed-message-event
|
||||
[^js self session-id runtime-session-id ts]
|
||||
(when (string? runtime-session-id)
|
||||
(let [state (ensure-acp-turn-state! self runtime-session-id)
|
||||
turn (or (aget state "turn") 1)
|
||||
text (some-> (aget state "text") str string/trim not-empty)
|
||||
reasoning (some-> (aget state "reasoning") str string/trim not-empty)
|
||||
parts (cond-> []
|
||||
(string? reasoning) (conj {:type "reasoning" :text reasoning})
|
||||
(string? text) (conj {:type "text" :text text}))]
|
||||
(when (seq parts)
|
||||
(aset state "turn" (inc turn))
|
||||
(aset state "text" "")
|
||||
(aset state "reasoning" "")
|
||||
(make-event session-id
|
||||
"item.completed"
|
||||
{:item_id (str runtime-session-id "-turn-" turn)
|
||||
:item {:item_id (str runtime-session-id "-turn-" turn)
|
||||
:kind "message"
|
||||
:role "assistant"
|
||||
:content parts}}
|
||||
ts)))))
|
||||
|
||||
(defn- current-acp-live-message
|
||||
[^js self session-id runtime-session-id ts]
|
||||
(when (string? runtime-session-id)
|
||||
(let [state (ensure-acp-turn-state! self runtime-session-id)]
|
||||
(acp-live-message-event session-id
|
||||
runtime-session-id
|
||||
(or (aget state "turn") 1)
|
||||
(aget state "text")
|
||||
(aget state "reasoning")
|
||||
ts))))
|
||||
|
||||
(defn- <append-event! [^js self event-opts]
|
||||
(p/let [session (<get-session self)]
|
||||
(if (nil? session)
|
||||
@@ -635,14 +794,66 @@
|
||||
(let [{:keys [type data]} (or (sandbox/acp-envelope->event payload)
|
||||
{:type (or (:type payload) "agent.runtime")
|
||||
:data payload})
|
||||
event-type type]
|
||||
(p/let [_ (<append-event! self {:type event-type
|
||||
:data data
|
||||
:ts (common/now-ms)})]
|
||||
(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")))))))
|
||||
event-type type
|
||||
ts (common/now-ms)
|
||||
live-event (make-event session-id event-type data ts)
|
||||
runtime-session-id (or (some-> data :session-id str string/trim not-empty)
|
||||
(some-> payload :params :sessionId str string/trim not-empty)
|
||||
(last-acp-runtime-session-id self))
|
||||
update-kind (some-> data :update :sessionUpdate str string/trim not-empty)
|
||||
stop-reason (some-> payload :result :stopReason str string/trim not-empty)]
|
||||
(cond
|
||||
(= "agent_message_chunk" update-kind)
|
||||
(do
|
||||
(remember-acp-runtime-session-id! self runtime-session-id)
|
||||
(append-acp-text! self runtime-session-id "text" (some-> data :update :content :text))
|
||||
(broadcast-event! self live-event)
|
||||
(p/let [latest-session (<get-session self)
|
||||
live-message (current-acp-live-message self session-id runtime-session-id ts)]
|
||||
(when (and (map? latest-session)
|
||||
(string? runtime-session-id)
|
||||
(map? live-message))
|
||||
(<save-session! self (assoc-live-message latest-session runtime-session-id live-message)))))
|
||||
|
||||
(= "agent_thought_chunk" update-kind)
|
||||
(do
|
||||
(remember-acp-runtime-session-id! self runtime-session-id)
|
||||
(append-acp-text! self runtime-session-id "reasoning" (some-> data :update :content :text))
|
||||
(broadcast-event! self live-event)
|
||||
(p/let [latest-session (<get-session self)
|
||||
live-message (current-acp-live-message self session-id runtime-session-id ts)]
|
||||
(when (and (map? latest-session)
|
||||
(string? runtime-session-id)
|
||||
(map? live-message))
|
||||
(<save-session! self (assoc-live-message latest-session runtime-session-id live-message)))))
|
||||
|
||||
(string? stop-reason)
|
||||
(let [completed-event (acp-completed-message-event self session-id runtime-session-id ts)]
|
||||
(broadcast-event! self live-event)
|
||||
(p/let [_ (when (map? completed-event)
|
||||
(<append-event! self completed-event))
|
||||
latest-session (<get-session self)]
|
||||
(when (and (map? latest-session)
|
||||
(string? runtime-session-id))
|
||||
(<save-session! self (dissoc-live-message latest-session runtime-session-id)))
|
||||
nil))
|
||||
|
||||
(runtime-delta-event? event-type)
|
||||
(broadcast-event! self live-event)
|
||||
|
||||
(runtime-transient-event? event-type)
|
||||
(broadcast-event! self live-event)
|
||||
|
||||
:else
|
||||
(p/let [_ (<append-event! self {:type event-type
|
||||
:data data
|
||||
:ts ts})]
|
||||
(when (= "session.completed" event-type)
|
||||
(clear-acp-turn-state! self runtime-session-id)
|
||||
(<checkpoint-and-terminate-completed-runtime! self session-id))
|
||||
(when (= "session.canceled" event-type)
|
||||
(clear-acp-turn-state! self runtime-session-id)
|
||||
(<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)]
|
||||
@@ -1007,7 +1218,7 @@
|
||||
(session/append-event session [] {:type "session.created"
|
||||
:data {:requested-by user-id
|
||||
:project (:project task)
|
||||
:agent (:agent task)}
|
||||
:agent (sanitize-agent-for-public (:agent task))}
|
||||
:ts now})]
|
||||
(p/let [_ (<put-session! self session)
|
||||
_ (<put-events! self events)
|
||||
@@ -1037,7 +1248,7 @@
|
||||
:status (:status session)
|
||||
:runtime-provider (session-runtime-provider session)
|
||||
:terminal-enabled (session-terminal-enabled? session)
|
||||
:task (:task session)
|
||||
:task (sanitize-task-for-public (:task session))
|
||||
:audit (:audit session)
|
||||
:created-at (:created-at session)
|
||||
:updated-at (:updated-at session)}))))
|
||||
@@ -1589,7 +1800,10 @@
|
||||
(if (nil? session)
|
||||
(http/not-found)
|
||||
(p/let [events (<get-events self)
|
||||
filtered (session/filter-events events {:since-ts since-ts :limit limit})]
|
||||
combined (into (vec events) (vals (session-live-messages session)))
|
||||
filtered (->> (session/filter-events combined {:since-ts since-ts :limit limit})
|
||||
(map sanitize-event-for-public)
|
||||
vec)]
|
||||
(http/json-response :sessions/events {:events filtered}))))))
|
||||
|
||||
(defn- handle-branches [^js self request]
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
(when-let [dir (get-repo-dir session-id task provider)]
|
||||
(str "cd '" (escape-shell-single dir) "'"))))
|
||||
|
||||
;; FIXME: sandbox-agent 2.x.x changes session routes to opencode/session
|
||||
(defn- sandbox-agent-version
|
||||
[^js env]
|
||||
(or (env-str env "SANDBOX_AGENT_VERSION")
|
||||
|
||||
@@ -97,9 +97,9 @@
|
||||
"cljs:release-electron": "clojure -M:cljs release app db-worker inference-worker electron --debug && clojure -M:cljs release publishing",
|
||||
"cljs:release-app": "clojure -M:cljs release app db-worker inference-worker",
|
||||
"cljs:release-publishing": "clojure -M:cljs release app publishing",
|
||||
"cljs:test": "clojure -M:test compile test",
|
||||
"cljs:test": "node scripts/clean-shadow-test-builds.js && clojure -M:test compile test",
|
||||
"cljs:run-test": "node scripts/run-tests-with-dom-shim.js -r '^(?!logseq.sync.).*' -r '^(?!logseq.agents.).*' -e fix-me",
|
||||
"cljs:test-no-worker": "clojure -M:test compile test-no-worker",
|
||||
"cljs:test-no-worker": "node scripts/clean-shadow-test-builds.js && clojure -M:test compile test-no-worker",
|
||||
"cljs:run-test-no-worker": "node scripts/run-tests-no-worker-with-dom-shim.js",
|
||||
"cljs:dev-release-app": "clojure -M:cljs release app db-worker inference-worker --config-merge \"{:closure-defines {frontend.config/DEV-RELEASE true}}\"",
|
||||
"cljs:dev-release-electron": "clojure -M:cljs release app db-worker inference-worker electron --debug --config-merge \"{:closure-defines {frontend.config/DEV-RELEASE true}}\" && clojure -M:cljs release publishing",
|
||||
|
||||
15
scripts/clean-shadow-test-builds.js
Normal file
15
scripts/clean-shadow-test-builds.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repoRoot = path.resolve(__dirname, "..");
|
||||
|
||||
const targets = [
|
||||
path.join(repoRoot, ".shadow-cljs", "builds", "test"),
|
||||
path.join(repoRoot, ".shadow-cljs", "builds", "test-no-worker"),
|
||||
path.join(repoRoot, "static", "tests.js"),
|
||||
path.join(repoRoot, "static", "tests-no-worker.js"),
|
||||
];
|
||||
|
||||
for (const target of targets) {
|
||||
fs.rmSync(target, { recursive: true, force: true });
|
||||
}
|
||||
@@ -178,6 +178,7 @@
|
||||
:closure-defines {frontend.util/NODETEST true
|
||||
logseq.shui.util/NODETEST true}
|
||||
:devtools {:enabled false}
|
||||
:build-options {:ns-exclude-regexp "^logseq\\.agents(\\.|$)"}
|
||||
;; disable :static-fns to allow for with-redefs and repl development
|
||||
:compiler-options {:static-fns false}
|
||||
:main frontend.test.frontend-node-test-runner/main}
|
||||
@@ -187,7 +188,7 @@
|
||||
:closure-defines {frontend.util/NODETEST true
|
||||
logseq.shui.util/NODETEST true}
|
||||
:devtools {:enabled false}
|
||||
:build-options {:ns-exclude-regexp "^(frontend\\.worker|logseq\\.sync\\.worker)"}
|
||||
:build-options {:ns-exclude-regexp "^(frontend\\.worker|logseq\\.sync\\.worker|logseq\\.agents(\\.|$))"}
|
||||
;; disable :static-fns to allow for with-redefs and repl development
|
||||
:compiler-options {:static-fns false}
|
||||
:main frontend.test.frontend-node-test-runner/main}
|
||||
|
||||
@@ -38,28 +38,102 @@
|
||||
(apply str)
|
||||
normalized-text)))
|
||||
|
||||
(defn- artifact-protocol-message?
|
||||
[text]
|
||||
(and (string? text)
|
||||
(string/includes? text "You are executing a coding task in a single session.")
|
||||
(string/includes? text "<logseq-plan>")
|
||||
(string/includes? text "<logseq-post-review>")))
|
||||
|
||||
(defn- leading-artifact-json-split
|
||||
[text]
|
||||
(let [text (or text "")
|
||||
leading-space (count (or (re-find #"^\s*" text) ""))
|
||||
len (count text)]
|
||||
(when (and (< leading-space len)
|
||||
(= \{ (nth text leading-space)))
|
||||
(loop [idx leading-space
|
||||
depth 0
|
||||
in-string? false
|
||||
escaped? false]
|
||||
(when (< idx len)
|
||||
(let [ch (nth text idx)]
|
||||
(cond
|
||||
escaped?
|
||||
(recur (inc idx) depth in-string? false)
|
||||
|
||||
(= ch \\)
|
||||
(recur (inc idx) depth in-string? true)
|
||||
|
||||
(= ch \")
|
||||
(recur (inc idx) depth (not in-string?) false)
|
||||
|
||||
in-string?
|
||||
(recur (inc idx) depth true false)
|
||||
|
||||
(= ch \{)
|
||||
(recur (inc idx) (inc depth) false false)
|
||||
|
||||
(= ch \})
|
||||
(let [next-depth (dec depth)]
|
||||
(if (zero? next-depth)
|
||||
[(subs text leading-space (inc idx))
|
||||
(subs text (inc idx))]
|
||||
(recur (inc idx) next-depth false false)))
|
||||
|
||||
:else
|
||||
(recur (inc idx) depth false false))))))))
|
||||
|
||||
(defn- artifact-json?
|
||||
[text]
|
||||
(let [parsed (chat-event/parse-json-safe text)]
|
||||
(and (map? parsed)
|
||||
(or (contains? parsed :planMarkdown)
|
||||
(contains? parsed :reviewMarkdown)
|
||||
(contains? parsed :subtasks)))))
|
||||
|
||||
(defn- strip-leading-artifact-json
|
||||
[text]
|
||||
(if-let [[json-part remainder] (leading-artifact-json-split text)]
|
||||
(if (artifact-json? json-part)
|
||||
remainder
|
||||
text)
|
||||
text))
|
||||
|
||||
(defn- visible-chat-text
|
||||
[text]
|
||||
(when-let [text (normalized-text text)]
|
||||
(when-not (artifact-protocol-message? text)
|
||||
(-> text
|
||||
(string/replace #"(?s)<logseq-plan>\s*.*?\s*</logseq-plan>" "")
|
||||
(string/replace #"(?s)<logseq-post-review>\s*.*?\s*</logseq-post-review>" "")
|
||||
strip-leading-artifact-json
|
||||
normalized-text))))
|
||||
|
||||
(defn- normalize-message-part
|
||||
[part]
|
||||
(when (map? part)
|
||||
(if (= "text" (:type part))
|
||||
(when-let [text (normalized-text (or (:text part) (:content part)))]
|
||||
(when-let [text (visible-chat-text (or (:text part) (:content part)))]
|
||||
{:type "text"
|
||||
:text text})
|
||||
part)))
|
||||
|
||||
(defn- ui-message-text
|
||||
[message]
|
||||
(or (text-from-content-parts (:parts message))
|
||||
(or (some-> (text-from-content-parts (:parts message))
|
||||
visible-chat-text)
|
||||
(let [content (:content message)]
|
||||
(cond
|
||||
(string? content)
|
||||
(normalized-text content)
|
||||
(visible-chat-text content)
|
||||
|
||||
(seq content)
|
||||
(text-from-content-parts content)
|
||||
(some-> (text-from-content-parts content)
|
||||
visible-chat-text)
|
||||
|
||||
:else nil))
|
||||
(normalized-text (:text message))))
|
||||
(visible-chat-text (:text message))))
|
||||
|
||||
(defn- normalize-role [role]
|
||||
(cond
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[frontend.handler.property :as property-handler]
|
||||
[frontend.handler.property.util :as pu]
|
||||
[frontend.handler.user :as user-handler]
|
||||
[frontend.modules.agent-chat.event :as chat-event]
|
||||
[frontend.state :as state]
|
||||
[frontend.util :as util]
|
||||
[lambdaisland.glogi :as log]
|
||||
@@ -41,9 +42,12 @@
|
||||
|
||||
(declare <start-session!)
|
||||
|
||||
(def ^:private task-agent-plan-property :logseq.property/agent-plan)
|
||||
(def ^:private task-sandbox-checkpoint-property :logseq.property/sandbox-checkpoint)
|
||||
(def ^:private task-session-id-property :logseq.property/agent-session-id)
|
||||
(def ^:private task-pr-property :logseq.property/pr)
|
||||
(def ^:private task-post-review-property :logseq.property/post-review)
|
||||
(defonce ^:private *artifact-buffers (atom {}))
|
||||
|
||||
(defn- normalize-sandbox-checkpoint
|
||||
[checkpoint]
|
||||
@@ -79,6 +83,21 @@
|
||||
[agent]
|
||||
(= "codex" (some-> (:provider agent) blank->nil string/lower-case)))
|
||||
|
||||
(defn- artifact-protocol-content
|
||||
[content]
|
||||
(str
|
||||
"You are executing a coding task in a single session.\n"
|
||||
"Before making code changes, output exactly one planning artifact using this XML tag:\n"
|
||||
"<logseq-plan>{\"planMarkdown\":\"## Plan\\n- concise implementation plan\",\"subtasks\":[{\"title\":\"<meaningful subtask title>\",\"status\":\"todo\"},{\"title\":\"<another meaningful subtask title>\",\"status\":\"doing\"}]}</logseq-plan>\n"
|
||||
"If the task should be split, list the proposed smaller tasks in `subtasks` and summarize them in `planMarkdown`. Use an empty `subtasks` array when no split is needed.\n"
|
||||
"Do not use placeholder titles like `subtask 1`, `subtask 2`, or similar template text.\n"
|
||||
"After emitting <logseq-plan>, continue implementation in the same session without waiting for confirmation.\n"
|
||||
"When the task is finished, output exactly one final post-review artifact:\n"
|
||||
"<logseq-post-review>{\"reviewMarkdown\":\"## Post-review\\n- what changed\\n- tests run\\n- risks\"}</logseq-post-review>\n"
|
||||
"Outside these XML tags, continue normal coding communication.\n\n"
|
||||
"Task source:\n\n"
|
||||
(or content "")))
|
||||
|
||||
(defn- project-config
|
||||
([project-page]
|
||||
(project-config project-page nil))
|
||||
@@ -316,7 +335,8 @@
|
||||
(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 (some-> block-uuid str)]
|
||||
session-id (some-> block-uuid str)
|
||||
content (artifact-protocol-content content)]
|
||||
(when (and session-id node-id (string? node-title) (string? content) (map? project) (map? agent))
|
||||
(cond-> {:session-id session-id
|
||||
:node-id node-id
|
||||
@@ -472,6 +492,175 @@
|
||||
task-sandbox-checkpoint-property
|
||||
(checkpoint->property-value checkpoint))))))))
|
||||
|
||||
(defn- event-text
|
||||
[event]
|
||||
(let [payload (chat-event/unwrap-event-payload
|
||||
{:data (if (map? (:data event)) (:data event) {})})
|
||||
item (:item payload)]
|
||||
(or (chat-event/payload-text item)
|
||||
(chat-event/payload-text payload)
|
||||
(blank->nil (get-in event [:data :last-agent-message])))))
|
||||
|
||||
(defn- extract-tagged-json
|
||||
[text tag]
|
||||
(let [text (or text "")
|
||||
pattern (js/RegExp. (str "<" tag ">\\s*([\\s\\S]*?)\\s*</" tag ">"))]
|
||||
(when-let [match (.exec pattern text)]
|
||||
(let [raw-json (aget match 1)
|
||||
parsed (chat-event/parse-json-safe raw-json)]
|
||||
(when (map? parsed)
|
||||
parsed)))))
|
||||
|
||||
(defn- normalize-plan-markdown
|
||||
[payload]
|
||||
(blank->nil (:planMarkdown payload)))
|
||||
|
||||
(defn- normalize-post-review-markdown
|
||||
[payload]
|
||||
(blank->nil (:reviewMarkdown payload)))
|
||||
|
||||
(defn- subtask-status-ident
|
||||
[subtask]
|
||||
(case (some-> (:status subtask) str string/lower-case)
|
||||
"backlog" :logseq.property/status.backlog
|
||||
"doing" :logseq.property/status.doing
|
||||
"in-review" :logseq.property/status.in-review
|
||||
"done" :logseq.property/status.done
|
||||
"canceled" :logseq.property/status.canceled
|
||||
:logseq.property/status.todo))
|
||||
|
||||
(defn- placeholder-subtask-title?
|
||||
[title]
|
||||
(let [title (some-> title blank->nil string/lower-case)]
|
||||
(boolean
|
||||
(and title
|
||||
(or (re-matches #"subtask\s*\d+" title)
|
||||
(re-matches #"task\s*\d+" title)
|
||||
(re-matches #"step\s*\d+" title)
|
||||
(string/includes? title "<meaningful subtask title>")
|
||||
(string/includes? title "<another meaningful subtask title>")
|
||||
(string/includes? title "<title>"))))))
|
||||
|
||||
(defn- normalize-subtasks
|
||||
[payload]
|
||||
(->> (:subtasks payload)
|
||||
(keep (fn [subtask]
|
||||
(cond
|
||||
(string? subtask)
|
||||
(when-let [title (blank->nil subtask)]
|
||||
(when-not (placeholder-subtask-title? title)
|
||||
{:title title
|
||||
:status :logseq.property/status.todo}))
|
||||
|
||||
(map? subtask)
|
||||
(when-let [title (or (blank->nil (:title subtask))
|
||||
(blank->nil (:name subtask)))]
|
||||
(when-not (placeholder-subtask-title? title)
|
||||
{:title title
|
||||
:status (subtask-status-ident subtask)}))
|
||||
|
||||
:else nil)))
|
||||
(reduce (fn [acc {:keys [title] :as subtask}]
|
||||
(assoc acc title subtask))
|
||||
{})
|
||||
vals
|
||||
vec))
|
||||
|
||||
(defn- append-artifact-buffer!
|
||||
[block-uuid text]
|
||||
(let [buffer-key (str block-uuid)]
|
||||
(get (swap! *artifact-buffers update buffer-key (fnil str "") (or text ""))
|
||||
buffer-key)))
|
||||
|
||||
(defn- artifact-text
|
||||
[block event]
|
||||
(let [block-uuid (:block/uuid block)
|
||||
chunk-kind (chat-event/acp-runtime-update-kind event)
|
||||
chunk-text (when (= "agent_message_chunk" chunk-kind)
|
||||
(chat-event/acp-runtime-update-text event))
|
||||
stop-reason (chat-event/acp-runtime-stop-reason event)
|
||||
message-complete? (or (contains? #{"item.completed" "response.completed" "session.completed"} (:type event))
|
||||
(string? stop-reason))
|
||||
event-text' (event-text event)]
|
||||
(cond
|
||||
(string? chunk-text)
|
||||
(do
|
||||
(append-artifact-buffer! block-uuid chunk-text)
|
||||
nil)
|
||||
|
||||
(and message-complete?
|
||||
(string? event-text'))
|
||||
(str (get @*artifact-buffers (str block-uuid) "")
|
||||
event-text')
|
||||
|
||||
message-complete?
|
||||
(get @*artifact-buffers (str block-uuid) "")
|
||||
|
||||
:else
|
||||
nil)))
|
||||
|
||||
(defn- child-block-with-title
|
||||
[block title]
|
||||
(some (fn [child]
|
||||
(when (= title (:block/title child))
|
||||
child))
|
||||
(db/sort-by-order (:block/_parent (db/entity [:block/uuid (:block/uuid block)])))))
|
||||
|
||||
(defn- maybe-set-block-property!
|
||||
[block-uuid property-id value]
|
||||
(when-let [block (db/entity [:block/uuid block-uuid])]
|
||||
(let [current (pu/get-block-property-value block property-id)]
|
||||
(when (not= current value)
|
||||
(property-handler/set-block-property! block-uuid property-id value)))))
|
||||
|
||||
(defn- copy-parent-task-properties!
|
||||
[parent-block child-uuid status-ident]
|
||||
(when-let [project (:logseq.property/project parent-block)]
|
||||
(maybe-set-block-property! child-uuid :logseq.property/project (:db/id project)))
|
||||
(when-let [agent (:logseq.property/agent parent-block)]
|
||||
(maybe-set-block-property! child-uuid :logseq.property/agent (:db/id agent)))
|
||||
(maybe-set-block-property! child-uuid :block/tags :logseq.class/Task)
|
||||
(maybe-set-block-property! child-uuid :logseq.property/status (or status-ident :logseq.property/status.todo)))
|
||||
|
||||
(defn- <ensure-split-subtasks!
|
||||
[block subtasks]
|
||||
(reduce (fn [promise {:keys [title status]}]
|
||||
(p/let [_ promise]
|
||||
(if-let [existing (child-block-with-title block title)]
|
||||
(do
|
||||
(copy-parent-task-properties! block (:block/uuid existing) status)
|
||||
nil)
|
||||
(p/let [created (editor-handler/api-insert-new-block! title
|
||||
{:block-uuid (:block/uuid block)
|
||||
:edit-block? false})]
|
||||
(copy-parent-task-properties! block (:block/uuid created) status)
|
||||
nil))))
|
||||
(p/resolved nil)
|
||||
subtasks))
|
||||
|
||||
(defn <sync-task-artifacts-from-event!
|
||||
[block event]
|
||||
(let [text (artifact-text block event)
|
||||
plan-payload (extract-tagged-json text "logseq-plan")
|
||||
review-payload (extract-tagged-json text "logseq-post-review")
|
||||
block-uuid (:block/uuid block)
|
||||
plan-md (normalize-plan-markdown plan-payload)
|
||||
subtasks (when (map? plan-payload)
|
||||
(normalize-subtasks plan-payload))
|
||||
post-review-md (normalize-post-review-markdown review-payload)
|
||||
stop-reason (chat-event/acp-runtime-stop-reason event)
|
||||
message-complete? (or (contains? #{"item.completed" "response.completed" "session.completed"} (:type event))
|
||||
(string? stop-reason))]
|
||||
(p/let [_ (when (and block-uuid (string? plan-md))
|
||||
(maybe-set-block-property! block-uuid task-agent-plan-property plan-md))
|
||||
_ (when (and block-uuid (seq subtasks))
|
||||
(<ensure-split-subtasks! block subtasks))
|
||||
_ (when (and block-uuid (string? post-review-md))
|
||||
(maybe-set-block-property! block-uuid task-post-review-property post-review-md))]
|
||||
(when message-complete?
|
||||
(swap! *artifact-buffers dissoc (str block-uuid)))
|
||||
nil)))
|
||||
|
||||
(defn- update-session!
|
||||
[block-uuid f]
|
||||
(state/update-state! :agent/sessions
|
||||
@@ -575,6 +764,11 @@
|
||||
(p/then (fn [resp]
|
||||
(when (seq (:events resp))
|
||||
(append-events! block-uuid (:events resp))
|
||||
(reduce (fn [promise event]
|
||||
(p/let [_ promise]
|
||||
(<sync-task-artifacts-from-event! block event)))
|
||||
(p/resolved nil)
|
||||
(:events resp))
|
||||
(let [provider (some->> (:events resp)
|
||||
reverse
|
||||
(keep event-runtime-provider)
|
||||
@@ -635,6 +829,9 @@
|
||||
[block-uuid event]
|
||||
(when-not (seen-event? block-uuid event)
|
||||
(append-events! block-uuid [event])
|
||||
(when-let [block (db/entity [:block/uuid block-uuid])]
|
||||
(-> (<sync-task-artifacts-from-event! block event)
|
||||
(.catch (fn [_] nil))))
|
||||
(when-let [provider (event-runtime-provider event)]
|
||||
(update-session-state! block-uuid {:runtime-provider provider
|
||||
:terminal-enabled (runtime-provider-terminal-enabled? provider)}))
|
||||
|
||||
@@ -85,7 +85,9 @@
|
||||
["65.25" {:properties [:logseq.property/pr]}]
|
||||
["65.27" {:properties [:logseq.property/agent-session-id]}]
|
||||
["65.28" {:properties [:logseq.property/sandbox-checkpoint]}]
|
||||
["65.29" {:properties [:logseq.property/project-sandbox-docker-file]}]])
|
||||
["65.29" {:properties [:logseq.property/project-sandbox-docker-file]}]
|
||||
["65.30" {:properties [:logseq.property/agent-plan
|
||||
:logseq.property/post-review]}]])
|
||||
|
||||
(let [[major minor] (last (sort (map (comp (juxt :major :minor) db-schema/parse-schema-version first)
|
||||
schema-version->updates)))]
|
||||
|
||||
@@ -49,6 +49,37 @@
|
||||
result (#'agent-chat/message->chat-message message)]
|
||||
(is (nil? result)))))
|
||||
|
||||
(deftest message->chat-message-hides-artifact-protocol-prompt-test
|
||||
(testing "drops the hidden protocol prompt from chat UI"
|
||||
(let [message {:id "m-protocol"
|
||||
:role "user"
|
||||
:parts [{:type "text"
|
||||
:text (str
|
||||
"You are executing a coding task in a single session.\n"
|
||||
"<logseq-plan>{\"planMarkdown\":\"## Plan\"}</logseq-plan>\n"
|
||||
"<logseq-post-review>{\"reviewMarkdown\":\"## Review\"}</logseq-post-review>")}]}
|
||||
result (#'agent-chat/message->chat-message message)]
|
||||
(is (nil? result)))))
|
||||
|
||||
(deftest session->messages-strips-leading-artifact-json-from-assistant-text-test
|
||||
(testing "keeps only the visible assistant narration after a hidden plan artifact"
|
||||
(let [session {:events [{:type "item.completed"
|
||||
:ts 1100
|
||||
:data {:item_id "itm-artifact"
|
||||
:item {:item_id "itm-artifact"
|
||||
:kind "message"
|
||||
:role "assistant"
|
||||
:content [{:type "text"
|
||||
:text (str
|
||||
"{\"planMarkdown\":\"## Plan\\n- Real plan\",\"subtasks\":[{\"title\":\"Build board\",\"status\":\"todo\"}]}"
|
||||
"The workspace is minimal and I’m reading the app structure now.")}]}}}]}
|
||||
messages (#'agent-chat/session->messages session {:block/uuid "b-artifact"})]
|
||||
(is (= [{:id "itm-artifact"
|
||||
:role "assistant"
|
||||
:parts [{:type "text"
|
||||
:text "The workspace is minimal and I’m reading the app structure now."}]}]
|
||||
messages)))))
|
||||
|
||||
(deftest session->messages-keeps-reasoning-and-tool-parts-from-item-content-test
|
||||
(testing "maps item.content parts to AI element friendly message parts"
|
||||
(let [session {:events [{:type "item.completed"
|
||||
|
||||
171
src/test/frontend/handler/agent_test.cljs
Normal file
171
src/test/frontend/handler/agent_test.cljs
Normal file
@@ -0,0 +1,171 @@
|
||||
(ns frontend.handler.agent-test
|
||||
(:require [cljs.test :refer [async deftest is testing use-fixtures]]
|
||||
[frontend.db :as db]
|
||||
[frontend.handler.agent :as agent-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 test-helper/destroy-test-db!})
|
||||
|
||||
(defn- setup-source-task!
|
||||
[]
|
||||
(test-helper/load-test-files
|
||||
[{:page {:block/title "Planning"}
|
||||
:blocks [{:block/title "Parent planning task"
|
||||
:build/properties {:block/tags #{:logseq.class/Task}}}]}])
|
||||
{:source (test-helper/find-block-by-content "Parent planning task")})
|
||||
|
||||
(deftest build-session-body-includes-planning-workflow-test
|
||||
(testing "execution prompts require plan and post-review artifacts in the same session"
|
||||
(let [{:keys [source]} (setup-source-task!)
|
||||
block-uuid (:block/uuid source)
|
||||
body (with-redefs [agent-handler/task-context
|
||||
(fn [_block _opts]
|
||||
{:block-uuid block-uuid
|
||||
:node-id (str block-uuid)
|
||||
:node-title "Parent planning task"
|
||||
:content "- Parent planning task"
|
||||
:attachments []
|
||||
:sandbox-checkpoint nil
|
||||
:project {:id "project-1"
|
||||
:title "Demo Project"
|
||||
:repo-url "https://github.com/logseq/logseq"}
|
||||
:agent {:provider "codex"}})]
|
||||
(agent-handler/build-session-body source))]
|
||||
(is (string? (:content body)))
|
||||
(is (re-find #"<logseq-plan>" (:content body)))
|
||||
(is (re-find #"<logseq-post-review>" (:content body)))
|
||||
(is (re-find #"continue implementation in the same session" (:content body)))
|
||||
(is (re-find #"reviewMarkdown" (:content body)))
|
||||
(is (not (re-find #"shouldSplit" (:content body)))))))
|
||||
|
||||
(deftest-async sync-task-artifacts-from-event-test
|
||||
(let [{:keys [source]} (setup-source-task!)
|
||||
plan-event {:type "item.completed"
|
||||
:data {:item {:kind "message"
|
||||
:content [{:type "text"
|
||||
:text (str
|
||||
"Planning phase done.\n"
|
||||
"<logseq-plan>{\"planMarkdown\":\"## Plan\\n- Inspect code\\n- Implement change\",\"subtasks\":[{\"title\":\"Extract artifact parser\",\"status\":\"todo\"},{\"title\":\"Persist post-review\",\"status\":\"doing\"}]}</logseq-plan>")}]}}}
|
||||
review-event {:type "session.completed"
|
||||
:data {:last-agent-message
|
||||
(str
|
||||
"Work complete.\n"
|
||||
"<logseq-post-review>{\"reviewMarkdown\":\"## Post-review\\n- Tests passed\\n- Risk: parser relies on tagged artifacts\"}</logseq-post-review>")}}]
|
||||
(p/let [_ (agent-handler/<sync-task-artifacts-from-event! source plan-event)
|
||||
updated (db/entity [:block/uuid (:block/uuid source)])
|
||||
_ (is (= "## Plan\n- Inspect code\n- Implement change"
|
||||
(pu/get-block-property-value updated :logseq.property/agent-plan)))
|
||||
child-statuses (->> (:block/_parent updated)
|
||||
(map (fn [child]
|
||||
[(:block/title child)
|
||||
(some-> (:logseq.property/status child) :db/ident)]))
|
||||
(into {}))
|
||||
_ (is (= :logseq.property/status.todo (get child-statuses "Extract artifact parser")))
|
||||
_ (is (= :logseq.property/status.doing (get child-statuses "Persist post-review")))
|
||||
_ (agent-handler/<sync-task-artifacts-from-event! source review-event)
|
||||
updated (db/entity [:block/uuid (:block/uuid source)])]
|
||||
(is (= "## Plan\n- Inspect code\n- Implement change"
|
||||
(pu/get-block-property-value updated :logseq.property/agent-plan)))
|
||||
(is (= "## Post-review\n- Tests passed\n- Risk: parser relies on tagged artifacts"
|
||||
(pu/get-block-property-value updated :logseq.property/post-review))))))
|
||||
|
||||
(deftest chunked-plan-artifact-creates-subtasks-test
|
||||
(async done
|
||||
(let [{:keys [source]} (setup-source-task!)
|
||||
block-uuid (:block/uuid source)
|
||||
chunk-events [{:type "agent.runtime"
|
||||
:data {:method "session/update"
|
||||
:session-id "runtime-1"
|
||||
:update {:sessionUpdate "agent_message_chunk"
|
||||
:content {:type "text"
|
||||
:text "<logseq-plan>{\"planMarkdown\":\"## Plan\\n- Split work\",\"subtasks\":[{\"title\":\"Build board\",\"status\":\"todo\"},{\"title\":\"Handle rotation\",\"status\":\"doing\"}]}"}}}}
|
||||
{:type "agent.runtime"
|
||||
:data {:method "session/update"
|
||||
:session-id "runtime-1"
|
||||
:update {:sessionUpdate "agent_message_chunk"
|
||||
:content {:type "text"
|
||||
:text "</logseq-plan>"}}}}
|
||||
{:type "agent.runtime"
|
||||
:data {:jsonrpc "2.0"
|
||||
:id 4
|
||||
:result {:stopReason "end_turn"}}}]]
|
||||
(-> (reduce (fn [promise event]
|
||||
(p/let [_ promise]
|
||||
(agent-handler/<sync-task-artifacts-from-event! source event)))
|
||||
(p/resolved nil)
|
||||
chunk-events)
|
||||
(p/then (fn [_]
|
||||
(let [updated (db/entity [:block/uuid block-uuid])
|
||||
child-statuses (->> (:block/_parent updated)
|
||||
(map (fn [child]
|
||||
[(:block/title child)
|
||||
(some-> (:logseq.property/status child) :db/ident)]))
|
||||
(into {}))]
|
||||
(is (= "## Plan\n- Split work"
|
||||
(pu/get-block-property-value updated :logseq.property/agent-plan)))
|
||||
(is (= :logseq.property/status.todo (get child-statuses "Build board")))
|
||||
(is (= :logseq.property/status.doing (get child-statuses "Handle rotation"))))))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))))
|
||||
(p/finally (fn []
|
||||
(done)))))))
|
||||
|
||||
(deftest chunked-plan-artifact-does-not-write-before-message-completes-test
|
||||
(async done
|
||||
(let [{:keys [source]} (setup-source-task!)
|
||||
block-uuid (:block/uuid source)
|
||||
partial-events [{:type "agent.runtime"
|
||||
:data {:method "session/update"
|
||||
:session-id "runtime-1"
|
||||
:update {:sessionUpdate "agent_message_chunk"
|
||||
:content {:type "text"
|
||||
:text "<logseq-plan>{\"planMarkdown\":\"## Plan\\n- Split work\",\"subtasks\":[{\"title\":\"Build board\",\"status\":\"todo\"}]}"}}}}
|
||||
{:type "agent.runtime"
|
||||
:data {:method "session/update"
|
||||
:session-id "runtime-1"
|
||||
:update {:sessionUpdate "agent_message_chunk"
|
||||
:content {:type "text"
|
||||
:text "</logseq-plan>Trailing narrative that should not trigger writes yet."}}}}]]
|
||||
(-> (reduce (fn [promise event]
|
||||
(p/let [_ promise]
|
||||
(agent-handler/<sync-task-artifacts-from-event! source event)))
|
||||
(p/resolved nil)
|
||||
partial-events)
|
||||
(p/then (fn [_]
|
||||
(let [updated (db/entity [:block/uuid block-uuid])
|
||||
child-titles (->> (:block/_parent updated)
|
||||
(map :block/title)
|
||||
set)]
|
||||
(is (nil? (pu/get-block-property-value updated :logseq.property/agent-plan)))
|
||||
(is (not (contains? child-titles "Build board"))))))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))))
|
||||
(p/finally (fn []
|
||||
(done)))))))
|
||||
|
||||
(deftest placeholder-subtasks-are-ignored-test
|
||||
(async done
|
||||
(let [{:keys [source]} (setup-source-task!)
|
||||
block-uuid (:block/uuid source)
|
||||
event {:type "item.completed"
|
||||
:data {:item {:kind "message"
|
||||
:content [{:type "text"
|
||||
:text "<logseq-plan>{\"planMarkdown\":\"## Plan\\n- Real task\",\"subtasks\":[{\"title\":\"subtask 1\",\"status\":\"todo\"},{\"title\":\"subtask 2\",\"status\":\"doing\"}]}</logseq-plan>"}]}}}]
|
||||
(-> (agent-handler/<sync-task-artifacts-from-event! source event)
|
||||
(p/then (fn [_]
|
||||
(let [updated (db/entity [:block/uuid block-uuid])
|
||||
child-titles (->> (:block/_parent updated)
|
||||
(map :block/title)
|
||||
set)]
|
||||
(is (= "## Plan\n- Real task"
|
||||
(pu/get-block-property-value updated :logseq.property/agent-plan)))
|
||||
(is (not (contains? child-titles "subtask 1")))
|
||||
(is (not (contains? child-titles "subtask 2"))))))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))))
|
||||
(p/finally (fn []
|
||||
(done)))))))
|
||||
@@ -95,3 +95,23 @@
|
||||
(:kv/value (d/entity @conn :logseq.kv/schema-version))))
|
||||
(is (some? property))
|
||||
(is (= :default (:logseq.property/type property)))))
|
||||
|
||||
(deftest migrate-adds-planner-properties-builtin
|
||||
(let [conn (db-test/create-conn)
|
||||
property-idents [:logseq.property/agent-plan
|
||||
:logseq.property/post-review]
|
||||
_ (d/transact! conn [{:db/ident :logseq.kv/schema-version
|
||||
:kv/value {:major 65 :minor 29}}])
|
||||
_ (doseq [property-ident property-idents
|
||||
:let [existing-eid (d/entid @conn property-ident)]
|
||||
:when existing-eid]
|
||||
(d/transact! conn [[:db/retractEntity existing-eid]]))
|
||||
_ (db-migrate/migrate conn :target-version "65.30")
|
||||
agent-plan (d/entity @conn :logseq.property/agent-plan)
|
||||
post-review (d/entity @conn :logseq.property/post-review)]
|
||||
(is (= {:major 65 :minor 30}
|
||||
(:kv/value (d/entity @conn :logseq.kv/schema-version))))
|
||||
(is (some? agent-plan))
|
||||
(is (some? post-review))
|
||||
(is (= :default (:logseq.property/type agent-plan)))
|
||||
(is (= :default (:logseq.property/type post-review)))))
|
||||
|
||||
Reference in New Issue
Block a user