Repository navigation
Expand file tree
/
Copy pathdynamic_workflows.clj
More file actions
136 lines (124 loc) · 6.1 KB
/
Copy pathdynamic_workflows.clj
File metadata and controls
136 lines (124 loc) · 6.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
(ns dynamic-workflows
"Experimental Dynamic Workflows API: define a workflow, join the parent CLI
session as a child extension, and service reverse-RPC workflow runs until
the parent session ends.
A Dynamic Workflow is a named, reusable multi-step routine (with declared
phases and optional resource limits) that an extension registers on join. The
*parent* Copilot CLI session (or another script driving that session)
triggers runs by name; this process only defines and services them - it
never calls the run! side of the API itself."
(:require [github.copilot-sdk :as copilot]
[clojure.string :as str]))
;; See examples/README.md for usage. This example must run as a child
;; process of a live Copilot CLI session (SESSION_ID set in the
;; environment), so it is intentionally excluded from run-all-examples.sh.
(def defaults
{:workflow-name "clj-example-review"
:shutdown-timeout-ms 5000})
;; -----------------------------------------------------------------------
;; Workflow definition
;; -----------------------------------------------------------------------
(defn- validate-args!
"Validate the map a caller passes as workflow run args (`(:args context)`).
`define-workflow` already validates :meta/:phases/:limits; a workflow's
:run function is responsible for validating its own runtime args."
[args]
(when-not (map? args)
(throw (ex-info "Workflow args must be a map" {:args args})))
(let [topic (:topic args)]
(when-not (and (string? topic) (not (str/blank? topic)))
(throw (ex-info "Workflow args require a non-blank :topic string" {:args args})))
topic))
(defn- run-review-workflow
"The workflow's :run function. Receives the workflow-execution context map
(:args :agent :step :parallel :phase :log, among others) and returns a
JSON-safe result. Demonstrates phase, log, agent, step, and parallel."
[{:keys [args agent step parallel phase log]}]
(let [topic (validate-args! args)]
(phase "Plan")
(log (str "Planning review of: " topic))
(let [plan (agent (str "In one short sentence, outline how to review: " topic)
{:schema {"type" "object"
"properties" {"approach" {"type" "string"}}
"required" ["approach"]}})]
(phase "Gather")
(log "Collecting context and risks in parallel")
(let [[context risks]
(parallel [(fn [] (step "fetch-context"
(fn [] {:note (str "Context for " topic)})))
(fn [] (step "fetch-risks"
(fn [] {:note (str "Risks for " topic)})))])]
(phase "Summarize")
(log "Composing final summary")
{:topic topic
:plan plan
:context context
:risks risks}))))
(def review-workflow
"A WorkflowHandle registered with the parent session in `run` below."
(copilot/define-workflow
{:meta {:name (:workflow-name defaults)
:description "Reviews a topic across plan, gather, and summarize phases."
:phases [{:title "Plan" :detail "Outline the review approach"}
{:title "Gather" :detail "Collect context and risks in parallel"}
{:title "Summarize" :detail "Compose the final result"}]}
:run run-review-workflow}))
;; -----------------------------------------------------------------------
;; Extension entry point
;; -----------------------------------------------------------------------
(defn- run-with-timeout
"Run f with a timeout. Returns true if it completed, false otherwise."
[f timeout-ms]
(let [result (promise)
thread (Thread. (fn []
(try
(f)
(deliver result :ok)
(catch Exception _
(deliver result :error)))))]
(.start thread)
(let [r (deref result timeout-ms :timeout)]
(when (= r :timeout)
(try (.interrupt thread) (catch Exception _)))
(= r :ok))))
(defn- stop-once!
"Stop client at most once, guarded by the cleaned? atom so the graceful
exit path (below) and the JVM shutdown hook never both run stop! on an
already-stopped client."
[cleaned? client]
(when (compare-and-set! cleaned? false true)
(println "Cleaning up client...")
(let [stopped? (run-with-timeout #(copilot/stop! client)
(:shutdown-timeout-ms defaults))]
(when-not stopped?
(run-with-timeout #(copilot/force-stop! client)
(:shutdown-timeout-ms defaults))))))
(defn run
"Join the parent Copilot CLI session, register `review-workflow`, and block
until the parent session ends (or the process is interrupted).
Requires the SESSION_ID environment variable - see examples/README.md."
[_]
(when-not (System/getenv "SESSION_ID")
(throw (ex-info (str "dynamic-workflows requires SESSION_ID: run this example as a child "
"process of a live Copilot CLI extension session (see examples/README.md).")
{})))
(let [{:keys [client session]} (copilot/join-session {:workflows [review-workflow]})
parent-session-id (copilot/session-id session)
cleaned? (atom false)
done (promise)]
(println "Registered workflow" (str "\"" (:workflow-name defaults) "\"")
"on session" parent-session-id)
(.addShutdownHook (Runtime/getRuntime)
(Thread. (fn [] (stop-once! cleaned? client))))
(copilot/on-lifecycle-event client :session.deleted
(fn [event]
(when (= (:session-id event) parent-session-id)
(println "Parent session ended.")
(deliver done :session-ended))))
(println "Waiting for the parent session to trigger a workflow run")
(println " (e.g. via run-workflow! from another script or the CLI itself).")
(println " Ctrl-C, or end the parent session, to exit.")
(try
(deref done)
(finally
(stop-once! cleaned? client)))))