optiondesk-agent 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ """LangChain bindings for the option desk.
2
+
3
+ Licensed under the PolyForm Noncommercial License 1.0.0; see LICENSE.
4
+ This package is a language layer over the shell: it turns
5
+ commands into tools, artifacts into documents, and questions into prompts
6
+ that can only be answered from what is on disk.
7
+
8
+ It never computes a number. Everything quantitative comes from the engine,
9
+ which does not depend on this package.
10
+ """
11
+
12
+ from optiondesk_agent.artifacts import ArtifactStore
13
+ from optiondesk_agent.prompts import (
14
+ REPORTING_RULES,
15
+ build_answer_prompt,
16
+ build_router_prompt,
17
+ )
18
+ from optiondesk_agent.tools import desk_tools, tool_specs
19
+
20
+ # Imported lazily by name: the graph needs langgraph, which is an optional
21
+ # extra, and importing it here would make the whole package require it.
22
+ __lazy__ = ("build_desk_graph", "open_desk")
23
+
24
+
25
+ def __getattr__(name):
26
+ if name in __lazy__:
27
+ from optiondesk_agent import graph
28
+
29
+ return getattr(graph, name)
30
+ raise AttributeError(name)
31
+
32
+ __version__ = "0.3.0"
33
+
34
+ __all__ = ["ArtifactStore", "desk_tools", "tool_specs",
35
+ "build_answer_prompt", "build_router_prompt", "REPORTING_RULES",
36
+ "build_desk_graph", "open_desk", "__version__"]
@@ -0,0 +1,263 @@
1
+ """The artifact directory as retrievable documents.
2
+
3
+ Artifacts are JSON, often large, and a language model reading one whole is
4
+ mostly reading strike ladders. This turns each into a short document that
5
+ carries what a question is usually about: what it is, when it was made,
6
+ whether it is degraded, and the handful of numbers a person would quote.
7
+
8
+ The full artifact is always one path away, and the path is in the document,
9
+ so nothing here prevents a caller from reading the detail when the detail
10
+ is the point.
11
+ """
12
+
13
+ import json
14
+ from pathlib import Path
15
+
16
+ KINDS = ("chain", "greeks", "exposure", "strategy", "comparison",
17
+ "simulation", "backtest")
18
+
19
+
20
+ def _load(path):
21
+ try:
22
+ with open(path, encoding="utf-8") as handle:
23
+ payload = json.load(handle)
24
+ except (ValueError, OSError):
25
+ return None
26
+ return payload if isinstance(payload, dict) else None
27
+
28
+
29
+ def _percent(value, digits=2):
30
+ return "n/a" if value is None else "{:.{d}f}%".format(value * 100,
31
+ d=digits)
32
+
33
+
34
+ def _meta(payload):
35
+ """The artifact's envelope, or an empty one.
36
+
37
+ The envelope carries the degraded flag, so it is read on every path
38
+ that describes an artifact and is the one part that cannot be allowed
39
+ to fail. `payload.get("meta", {})` is not enough: it returns the value
40
+ when the key is present and null, so the default never fires, and a
41
+ meta that is present but is not an object fails the same way.
42
+ """
43
+ meta = payload.get("meta")
44
+ return meta if isinstance(meta, dict) else {}
45
+
46
+
47
+ def _summarise(kind, payload, path):
48
+ """One short paragraph per artifact, in the terms a person would use.
49
+
50
+ Never raises for any JSON object, which is all _load lets through. What
51
+ it cannot read it says it cannot read, because this runs over every
52
+ file in the directory in one pass and an exception here costs the
53
+ caller every other artifact as well.
54
+ """
55
+ meta = _meta(payload)
56
+ header = "{} for {}{}, generated {}".format(
57
+ kind, payload.get("underlying", "unknown"),
58
+ " expiry {}".format(payload["expiry"]) if payload.get("expiry")
59
+ else "", meta.get("generated_utc", "unknown"))
60
+ lines = [header]
61
+
62
+ if meta.get("degraded"):
63
+ lines.append("DEGRADED: {}".format(meta.get("degraded_reason")))
64
+ for note in (meta.get("notes") or [])[:3]:
65
+ lines.append("note: {}".format(note))
66
+
67
+ # The lines above are the ones the reporting rules turn on: what this
68
+ # is, when it was made, and whether it is degraded. They are built
69
+ # before this point and kept whatever happens next.
70
+ #
71
+ # Nothing validates an artifact on the way in. records() accepts any
72
+ # JSON object whose filename prefix is a known kind, so a file written
73
+ # by an older schema, or by something that is not the desk at all,
74
+ # reaches here intact. Reading a field it does not have used to raise
75
+ # through context_for, which summarises the whole directory in one
76
+ # pass: one bad file and the model was told nothing about any
77
+ # underlying. Losing this artifact's numbers is the smaller failure,
78
+ # and saying so is what keeps it from reading as a clean artifact.
79
+ try:
80
+ if kind == "chain":
81
+ counts = payload.get("counts", {})
82
+ lines.append(
83
+ "spot {} as of {}. {} contracts, {} with a usable implied "
84
+ "volatility, {} without.".format(
85
+ payload.get("spot"), payload.get("spot_asof"),
86
+ len(payload.get("contracts", [])), counts.get("with_iv"),
87
+ counts.get("without_iv")))
88
+ elif kind == "greeks":
89
+ rows = payload.get("rows", [])
90
+ skipped = payload.get("skipped") or {}
91
+ lines.append(
92
+ "{} contracts graded, {} skipped for having no implied "
93
+ "volatility.".format(len(rows), skipped.get("no_iv")))
94
+ if rows:
95
+ spot = payload.get("spot")
96
+ atm = min(rows, key=lambda r: abs(r["strike"] - spot))
97
+ lines.append(
98
+ "at the money {} strike {}: iv {}, delta {:.4f}, gamma "
99
+ "{:.5f}, vega {:.2f}, theta {:.3f} per day.".format(
100
+ atm["type"], atm["strike"], _percent(atm.get("iv")),
101
+ atm.get("delta", 0), atm.get("gamma", 0),
102
+ atm.get("vega", 0), atm.get("theta", 0)))
103
+ elif kind == "exposure":
104
+ exposure = payload.get("exposure", {})
105
+ smile = payload.get("smile") or {}
106
+ pain = payload.get("max_pain") or {}
107
+ lines.append(
108
+ "net gamma exposure {:.0f} per one percent move, regime {}. "
109
+ "call wall {}, put wall {}, flip {}. max pain {}. put to call "
110
+ "open interest {}.".format(
111
+ exposure.get("net_gex", 0), exposure.get("regime"),
112
+ (exposure.get("call_wall") or {}).get("strike"),
113
+ (exposure.get("put_wall") or {}).get("strike"),
114
+ exposure.get("gamma_flip"), pain.get("strike"),
115
+ exposure.get("put_call_oi_ratio")))
116
+ lines.append(
117
+ "at the money volatility {}, 25 delta risk reversal {}, "
118
+ "expected move {}.".format(
119
+ _percent(smile.get("atm_iv")),
120
+ _percent(smile.get("risk_reversal")),
121
+ smile.get("expected_move")))
122
+ lines.append("assumption: {}".format(exposure.get("assumption")))
123
+ elif kind == "strategy":
124
+ analysis = payload.get("analysis", {})
125
+ lines.append(
126
+ "{} {}: net {}, breakevens {}, max gain {}, max "
127
+ "loss {}.".format(
128
+ payload.get("strategy"), analysis.get("trade_type"),
129
+ analysis.get("net_cash"), analysis.get("breakevens"),
130
+ analysis.get("max_gain"), analysis.get("max_loss")))
131
+ probability = payload.get("probability") or {}
132
+ if probability.get("profit") is not None:
133
+ lines.append("model probability of profit {}.".format(
134
+ _percent(probability["profit"], 1)))
135
+ friction = payload.get("friction") or {}
136
+ if friction.get("verdict"):
137
+ lines.append("friction {}: {}".format(friction["verdict"],
138
+ friction.get("reason")))
139
+ elif kind == "comparison":
140
+ leader = payload.get("leader") or {}
141
+ lines.append("{} structures ranked, {} excluded. Leader {} at {} "
142
+ "expected return on capital at risk.".format(
143
+ payload.get("rankable_count"),
144
+ payload.get("excluded_count"),
145
+ leader.get("strategy"),
146
+ _percent(
147
+ leader.get("expected_return_on_risk"), 1)))
148
+ lines.append("criterion: {}".format(payload.get("criterion")))
149
+ lines.append("caveat: {}".format(payload.get("caveat")))
150
+ elif kind == "simulation":
151
+ posterior = payload.get("posterior", {})
152
+ risk = payload.get("risk") or {}
153
+ simulation = payload.get("simulation", {})
154
+ lines.append(
155
+ "horizon {} days, {} paths, converged {}. value at risk "
156
+ "95 {}, expected shortfall 95 {}.".format(
157
+ simulation.get("horizon_days"), simulation.get("paths"),
158
+ posterior.get("converged"), _percent(risk.get("var_95")),
159
+ _percent(risk.get("es_95"))))
160
+ for structure in (payload.get("structures") or [])[:5]:
161
+ lines.append(
162
+ "{}: probability of profit {} under realised volatility "
163
+ "against {} under implied.".format(
164
+ structure.get("strategy"),
165
+ _percent(structure.get(
166
+ "realised_vol_probability_of_profit"), 1),
167
+ _percent(structure.get(
168
+ "implied_vol_probability_of_profit"), 1)))
169
+ elif kind == "backtest":
170
+ statistics = payload.get("statistics") or {}
171
+ significance = payload.get("significance") or {}
172
+ lines.append(
173
+ "{} over {} trades: win rate {}, mean return on risk {}, "
174
+ "p value {}.".format(
175
+ payload.get("strategy"), statistics.get("trades"),
176
+ _percent(statistics.get("win_rate"), 1),
177
+ _percent(statistics.get("mean_return")),
178
+ significance.get("p_value")))
179
+ lines.append("honesty: {}".format(payload.get("honesty")))
180
+ except Exception as exc:
181
+ lines.append(
182
+ "detail unavailable: this {} artifact does not match the "
183
+ "current schema, so no number is quoted from it ({}: {}). "
184
+ "Re-run the command that writes it."
185
+ .format(kind, type(exc).__name__, exc))
186
+
187
+ lines.append("full artifact: {}".format(path))
188
+ return "\n".join(str(line) for line in lines)
189
+
190
+
191
+ class ArtifactStore:
192
+ """Read only view of the artifact directory, as documents."""
193
+
194
+ def __init__(self, directory=None):
195
+ if directory is None:
196
+ from optiondesk.artifacts import artifact_dir
197
+
198
+ directory = artifact_dir()
199
+ self.directory = Path(directory)
200
+
201
+ def records(self, underlying=None, kinds=None):
202
+ """Every artifact as (kind, payload, path), newest first.
203
+
204
+ The sort key stats each file, and the directory is written to by
205
+ the commands while this reads it, so a file can vanish between the
206
+ glob and the stat. That raised FileNotFoundError out of here,
207
+ which is a failure of the whole read rather than of one artifact,
208
+ and it would land on whoever happened to be summarising while a
209
+ refresh ran. A file that is gone sorts last and is then dropped by
210
+ _load, which is what should happen to it.
211
+ """
212
+ if not self.directory.exists():
213
+ return []
214
+
215
+ def _age(path):
216
+ try:
217
+ return path.stat().st_mtime
218
+ except OSError:
219
+ return float("-inf")
220
+
221
+ out = []
222
+ for path in sorted(self.directory.glob("*.json"), key=_age,
223
+ reverse=True):
224
+ kind = path.name.split("_")[0]
225
+ if kind not in KINDS or (kinds and kind not in kinds):
226
+ continue
227
+ payload = _load(path)
228
+ if payload is None:
229
+ continue
230
+ if underlying and str(payload.get("underlying", "")).upper() \
231
+ != underlying.upper():
232
+ continue
233
+ out.append((kind, payload, str(path)))
234
+ return out
235
+
236
+ def documents(self, underlying=None, kinds=None):
237
+ """LangChain documents, one per artifact."""
238
+ from langchain_core.documents import Document
239
+
240
+ documents = []
241
+ for kind, payload, path in self.records(underlying, kinds):
242
+ documents.append(Document(
243
+ page_content=_summarise(kind, payload, path),
244
+ metadata={
245
+ "kind": kind,
246
+ "underlying": payload.get("underlying"),
247
+ "expiry": payload.get("expiry"),
248
+ "generated_utc": _meta(payload).get("generated_utc"),
249
+ "degraded": bool(_meta(payload).get("degraded")),
250
+ "path": path,
251
+ },
252
+ ))
253
+ return documents
254
+
255
+ def context_for(self, underlying=None, kinds=None, limit=12):
256
+ """A single context string, newest first, ready for a prompt."""
257
+ blocks = []
258
+ for kind, payload, path in self.records(underlying, kinds)[:limit]:
259
+ blocks.append(_summarise(kind, payload, path))
260
+ if not blocks:
261
+ return ("No artifacts are on disk for this request. Nothing can "
262
+ "be answered from data that has not been pulled.")
263
+ return "\n\n".join(blocks)
@@ -0,0 +1,341 @@
1
+ """A desk routine as a state graph, with a bounded loop.
2
+
3
+ Copyright (C) 2026 Iman Samizadeh. PolyForm Noncommercial 1.0.0, like the rest of this project.
4
+
5
+ WHAT THIS IS FOR. Opening a desk on an underlying is not one call, it is a
6
+ short dependency chain: a chain snapshot, then a Greek ladder from it, then
7
+ positioning, then structures, then a comparison. Each step needs the
8
+ previous one's artifact. Written as a script that is a sequence; written as
9
+ a graph it becomes something you can inspect, resume, and stop early when
10
+ the data says stop.
11
+
12
+ THE LOOP, AND WHY IT IS BOUNDED. The gather node runs one missing step per
13
+ visit and comes back. It loops until the artifact set is complete, the
14
+ budget is spent, or a step fails in a way repeating cannot fix. Three
15
+ distinct exits, because "it finished", "it ran out of turns" and "it broke"
16
+ are three different outcomes and collapsing them is how an agent ends up
17
+ reporting success on an empty directory.
18
+
19
+ There is no model in this graph. Every node calls a deterministic command
20
+ and reads an artifact, so the same inputs give the same outputs and a
21
+ failure is reproducible. A language model belongs at the edges, deciding
22
+ what to ask for and how to say what came back, and there is a node for that
23
+ which stays inert unless you pass one in.
24
+
25
+ WHAT COMPLETE DOES NOT MEAN. This graph reports that every stage produced
26
+ an artifact. It does not report that the artifacts are right, and those are
27
+ different claims. Three independent verifications in September 2026 found
28
+ eleven defects in numbers that every stage of this graph would have called
29
+ complete: a solver refusing contracts it could identify, significance tests
30
+ assuming an independence that overlapping windows do not have, a maximum
31
+ that was a property of its own scan window. The gather node checks that a
32
+ file appeared. Nothing here checks what is in it.
33
+
34
+ The report node carries degradation through for that reason. When a stage
35
+ writes an artifact marked degraded, the summary says so rather than
36
+ presenting a complete run, because "every stage finished" and "the figures
37
+ can be relied on" are the two claims most easily confused by anything
38
+ reading this output, human or otherwise.
39
+
40
+ Requires langgraph, which is an optional extra of this package.
41
+ """
42
+
43
+ import operator
44
+ from typing import Annotated, Any, TypedDict
45
+
46
+ from optiondesk_agent.artifacts import ArtifactStore
47
+
48
+ # The order matters: each step consumes what the previous one wrote.
49
+ PIPELINE = ("chain", "greeks", "exposure", "comparison")
50
+
51
+ DEFAULT_BUDGET = 8
52
+
53
+
54
+ class DeskState(TypedDict, total=False):
55
+ """What flows through the graph.
56
+
57
+ Kept flat and JSON-shaped so a checkpointer can serialise it and a
58
+ human can read a paused run.
59
+ """
60
+
61
+ underlying: str
62
+ expiry: str
63
+ band: float
64
+ budget: int
65
+ steps_taken: int
66
+ have: list
67
+ missing: list
68
+ log: Annotated[list, operator.add]
69
+ failures: Annotated[list, operator.add]
70
+ summary: str
71
+ outcome: str
72
+
73
+
74
+ def _present(store, underlying, expiry=None, steps=PIPELINE):
75
+ """Which of the named stages already have an artifact for this target.
76
+
77
+ steps narrows the scan. gather passes the single stage it just ran, so
78
+ verifying one runner's work costs one filtered read rather than a walk
79
+ of the whole directory.
80
+ """
81
+ have = []
82
+ for kind, payload, _ in store.records(underlying=underlying, kinds=steps):
83
+ if expiry and payload.get("expiry") not in (None, expiry):
84
+ continue
85
+ if kind not in have:
86
+ have.append(kind)
87
+ return have
88
+
89
+
90
+ def build_desk_graph(store=None, runners=None, model=None):
91
+ """Compile the routine.
92
+
93
+ store lets a test point at a temporary directory. runners lets a test
94
+ substitute the commands, which is the only way to exercise the loop
95
+ without the network. model is optional and only used by the report
96
+ node; without it the report is assembled from the artifacts alone.
97
+ """
98
+ from langgraph.graph import END, START, StateGraph
99
+
100
+ store = store or ArtifactStore()
101
+ runners = runners or _default_runners(store)
102
+
103
+ def plan(state: DeskState) -> DeskState:
104
+ underlying = state["underlying"]
105
+ have = _present(store, underlying, state.get("expiry"))
106
+ missing = [step for step in PIPELINE if step not in have]
107
+ return {
108
+ "have": have,
109
+ "missing": missing,
110
+ "budget": state.get("budget", DEFAULT_BUDGET),
111
+ "steps_taken": state.get("steps_taken", 0),
112
+ "log": ["plan: have {}, missing {}".format(
113
+ ", ".join(have) or "nothing", ", ".join(missing) or "nothing")],
114
+ }
115
+
116
+ def gather(state: DeskState) -> DeskState:
117
+ """Run exactly one missing step, then return to the decision.
118
+
119
+ One per visit rather than all of them, so the loop is visible in
120
+ the trace and a failure stops the run at the step that failed
121
+ instead of somewhere downstream.
122
+ """
123
+ missing = list(state.get("missing") or [])
124
+ # Unreachable through the compiled graph, and deliberately kept.
125
+ #
126
+ # Both edges into this node are the "continue" branch of decide (see
127
+ # the two add_conditional_edges calls below), and decide returns
128
+ # "continue" only after `if not state.get("missing")` has already
129
+ # returned "complete". So missing is non-empty on every real entry,
130
+ # and this line stays uncovered by design rather than by omission.
131
+ # test_graph.py proves both halves: that the only edges into gather
132
+ # carry the "continue" label, and that no run of any shape ever logs
133
+ # this line.
134
+ #
135
+ # It is not deleted because what protects missing[0] below is that
136
+ # routing invariant, not anything local to this function. A future
137
+ # edge from plan or report straight into gather would turn the line
138
+ # after this one into an IndexError, and an unexplained IndexError
139
+ # inside a loop is a much worse thing to debug than a log line.
140
+ if not missing:
141
+ return {"log": ["gather: nothing to do"]}
142
+ step = missing[0]
143
+ runner = runners.get(step)
144
+ if runner is None:
145
+ return {"failures": ["no runner for step {}".format(step)],
146
+ "steps_taken": state.get("steps_taken", 0) + 1}
147
+ try:
148
+ result = runner(state)
149
+ except Exception as exc:
150
+ return {
151
+ "failures": ["{} failed: {}: {}".format(
152
+ step, type(exc).__name__, exc)],
153
+ "steps_taken": state.get("steps_taken", 0) + 1,
154
+ "log": ["gather: {} raised".format(step)],
155
+ }
156
+
157
+ # A stage counts as done when its artifact is on disk, not when
158
+ # its runner returned. A command that exits cleanly without writing,
159
+ # or writes into a directory the store does not read, would
160
+ # otherwise drive the run to "complete" over an empty directory.
161
+ if step not in _present(store, state["underlying"],
162
+ state.get("expiry"), steps=(step,)):
163
+ return {
164
+ "failures": ["{} ran but wrote no artifact to {}".format(
165
+ step, store.directory)],
166
+ "steps_taken": state.get("steps_taken", 0) + 1,
167
+ "log": ["gather: {} wrote nothing".format(step)],
168
+ }
169
+
170
+ have = list(state.get("have") or [])
171
+ if step not in have:
172
+ have.append(step)
173
+ note = ""
174
+ if isinstance(result, dict):
175
+ if result.get("degraded"):
176
+ note = " (degraded: {})".format(result.get("degraded_reason"))
177
+ elif result.get("built") is False:
178
+ note = " (nothing viable)"
179
+ return {
180
+ "have": have,
181
+ "missing": [s for s in missing if s != step],
182
+ "steps_taken": state.get("steps_taken", 0) + 1,
183
+ "log": ["gather: ran {}{}".format(step, note)],
184
+ }
185
+
186
+ def decide(state: DeskState) -> str:
187
+ """Continue, stop because it is done, or stop because it cannot be.
188
+
189
+ Failures end the loop rather than being retried. Every failure this
190
+ pipeline produces is structural, a missing snapshot or an
191
+ unbuildable structure, and running the same command again produces
192
+ the same failure while spending budget that a different underlying
193
+ or expiry could have used.
194
+ """
195
+ if state.get("failures"):
196
+ return "failed"
197
+ if not state.get("missing"):
198
+ return "complete"
199
+ if state.get("steps_taken", 0) >= state.get("budget",
200
+ DEFAULT_BUDGET):
201
+ return "exhausted"
202
+ return "continue"
203
+
204
+ def report(state: DeskState) -> DeskState:
205
+ context = store.context_for(state["underlying"], limit=8)
206
+ outcome = state.get("_outcome") or (
207
+ "failed" if state.get("failures")
208
+ else "complete" if not state.get("missing") else "exhausted")
209
+ # Completeness is a claim about the directory, so it is checked
210
+ # against the directory. gather already refuses to mark a stage done
211
+ # without its artifact; this is the second lock, covering a set
212
+ # believed complete before the run whose artifacts went away
213
+ # underneath it.
214
+ extra_failures = []
215
+ if outcome == "complete" and not store.records(
216
+ underlying=state["underlying"]):
217
+ outcome = "failed"
218
+ extra_failures.append(
219
+ "every stage reported done but {} holds no artifact for "
220
+ "{}".format(store.directory, state["underlying"]))
221
+ failures = list(state.get("failures") or []) + extra_failures
222
+ header = {
223
+ "complete": "Desk open on {}. Every stage has an artifact.",
224
+ "exhausted": ("Desk partially open on {}. The step budget ran "
225
+ "out before the set was complete."),
226
+ "failed": "Desk did not open on {}. A step failed.",
227
+ }[outcome].format(state["underlying"])
228
+
229
+ # Degradation is carried into the summary rather than left in the
230
+ # artifacts for someone to find. "Every stage has an artifact" and
231
+ # "the figures can be relied on" are two claims, and a reader who
232
+ # sees only the first will hear the second. An audit of this
233
+ # project found eleven defects in numbers every stage of this graph
234
+ # would have called complete.
235
+ degraded = []
236
+ for kind, payload, _ in store.records(underlying=state["underlying"]):
237
+ meta = (payload or {}).get("meta") or {}
238
+ if meta.get("degraded"):
239
+ degraded.append(" {}: {}".format(
240
+ kind, meta.get("degraded_reason") or "degraded"))
241
+
242
+ body = [header, "", context]
243
+ if degraded:
244
+ body += ["", "Degraded stages, which the figures above inherit:"]
245
+ body += sorted(set(degraded))
246
+ if failures:
247
+ body += ["", "Failures:"] + [" " + f for f in failures]
248
+ summary = "\n".join(body)
249
+
250
+ if model is not None:
251
+ from optiondesk_agent.prompts import build_answer_prompt
252
+
253
+ prompt = build_answer_prompt()
254
+ summary = model.invoke(prompt.format_messages(
255
+ question="Summarise the state of this desk for a "
256
+ "professional reader.",
257
+ context=context)).content
258
+
259
+ return {"summary": summary, "outcome": outcome,
260
+ "failures": extra_failures}
261
+
262
+ graph = StateGraph(DeskState)
263
+ graph.add_node("plan", plan)
264
+ graph.add_node("gather", gather)
265
+ graph.add_node("report", report)
266
+ graph.add_edge(START, "plan")
267
+ graph.add_conditional_edges("plan", decide, {
268
+ "continue": "gather",
269
+ "complete": "report",
270
+ "exhausted": "report",
271
+ "failed": "report",
272
+ })
273
+ # The loop: gather returns to the same decision rather than to plan, so
274
+ # the whole artifact directory is not rescanned on every pass. gather
275
+ # does read the store for the one stage it just ran, because "done" has
276
+ # to mean the artifact exists rather than the runner returned, and one
277
+ # filtered read per step is the cheapest way to hold that line.
278
+ graph.add_conditional_edges("gather", decide, {
279
+ "continue": "gather",
280
+ "complete": "report",
281
+ "exhausted": "report",
282
+ "failed": "report",
283
+ })
284
+ graph.add_edge("report", END)
285
+ return graph.compile()
286
+
287
+
288
+ def _default_runners(store):
289
+ """The real commands, one per pipeline stage.
290
+
291
+ Every runner is told to write into the store's own directory. The store
292
+ is what gather then checks for the artifact, so a runner left writing
293
+ to the default location would report a stage done that the store cannot
294
+ see, which is the split that let a run complete over nothing.
295
+ """
296
+ from optiondesk.cli import chain as chain_cmd
297
+ from optiondesk.cli import compare as compare_cmd
298
+ from optiondesk.cli import exposure as exposure_cmd
299
+ from optiondesk.cli import greeks as greeks_cmd
300
+
301
+ class _Args:
302
+ def __init__(self, **kwargs):
303
+ for key, value in kwargs.items():
304
+ setattr(self, key, value)
305
+
306
+ out_dir = str(store.directory)
307
+
308
+ def run_chain(state):
309
+ return chain_cmd.run(_Args(
310
+ symbol=state["underlying"], expiry=state.get("expiry"),
311
+ provider=None, rate=None, dividend_yield=0.0, out_dir=out_dir))
312
+
313
+ def run_greeks(state):
314
+ return greeks_cmd.run(_Args(
315
+ snapshot=None, band=state.get("band", 0.06), type="both",
316
+ out_dir=out_dir))
317
+
318
+ def run_exposure(state):
319
+ return exposure_cmd.run(_Args(snapshot=None, multiplier=100.0,
320
+ out_dir=out_dir))
321
+
322
+ def run_comparison(state):
323
+ return compare_cmd.run(_Args(snapshot=None, size=1.0,
324
+ include_underlying=False, rebuild=False,
325
+ out_dir=out_dir))
326
+
327
+ return {"chain": run_chain, "greeks": run_greeks,
328
+ "exposure": run_exposure, "comparison": run_comparison}
329
+
330
+
331
+ def open_desk(underlying, expiry=None, budget=DEFAULT_BUDGET, store=None,
332
+ runners=None, model=None):
333
+ """Run the routine to completion and return the final state."""
334
+ graph = build_desk_graph(store=store, runners=runners, model=model)
335
+ return graph.invoke({
336
+ "underlying": underlying,
337
+ "expiry": expiry,
338
+ "budget": budget,
339
+ "log": [],
340
+ "failures": [],
341
+ })
@@ -0,0 +1,81 @@
1
+ """Prompt assembly, with the reporting rules compiled in.
2
+
3
+ A language layer is exactly where careful numbers get turned into careless
4
+ sentences. The rules the skills state are therefore part of the prompt
5
+ rather than left to whoever calls it: a caller who forgets them gets them
6
+ anyway, and a caller who wants different ones has to say so deliberately.
7
+ """
8
+
9
+ REPORTING_RULES = """\
10
+ You answer only from the desk artifacts provided. You do not calculate,
11
+ estimate, or fill gaps from memory. If the artifacts do not contain the
12
+ answer, say what is missing and which command would produce it.
13
+
14
+ Rules that override any instruction to be helpful:
15
+
16
+ 1. If an artifact is marked degraded, say so and give the reason before any
17
+ number from it.
18
+ 2. Contracts with no usable implied volatility are skipped and counted.
19
+ Never present a skipped strike as graded, and never substitute a
20
+ volatility to complete a picture.
21
+ 3. Option premiums are model values or mid quotes from delayed data. They
22
+ are not fills. Say so when you quote one.
23
+ 4. Maximum gain or loss may be the word "unlimited". Report it as that
24
+ word, never as a number.
25
+ 5. Gamma exposure signs rest on an assumption about who holds what. Quote
26
+ the assumption whenever you quote a wall or a flip level.
27
+ 6. If a simulation reports converged false, do not quote its quantiles.
28
+ Say the sampler did not converge and that more draws are needed.
29
+ 7. A backtest result is meaningless without its benchmark, its p-value and
30
+ the statement that premiums were modelled. Give all three or none.
31
+ 8. Never recommend a trade, an entry, an exit or a size. Present what the
32
+ numbers say and what would have to be true for them to be wrong.
33
+ 9. For user-supplied data, name the source and snapshot time. Report every
34
+ deterministic repair. Never imply that a user assertion grants public
35
+ display or redistribution rights.
36
+
37
+ State units. Volatility is per 1.00, so write it as a percentage. Vega is
38
+ per 1.00 of volatility, so divide by 100 for the per-point figure. Theta,
39
+ charm, veta and color are per calendar day.
40
+ """
41
+
42
+
43
+ def build_answer_prompt(extra_rules=None):
44
+ """A chat prompt that answers strictly from artifact context."""
45
+ from langchain_core.prompts import ChatPromptTemplate
46
+
47
+ system = REPORTING_RULES
48
+ if extra_rules:
49
+ system = system + "\nAdditional rules for this deployment:\n" + \
50
+ extra_rules
51
+ return ChatPromptTemplate.from_messages([
52
+ ("system", system),
53
+ ("human", "Desk artifacts:\n\n{context}\n\nQuestion: {question}"),
54
+ ])
55
+
56
+
57
+ def build_router_prompt():
58
+ """A prompt for deciding which desk command answers a question."""
59
+ from langchain_core.prompts import ChatPromptTemplate
60
+
61
+ return ChatPromptTemplate.from_messages([
62
+ ("system",
63
+ "You choose which desk capability answers a question, and nothing "
64
+ "else. Reply with one tool name and the arguments it needs.\n\n"
65
+ "option_snapshot_schema: accepted upload fields and repair rules.\n"
66
+ "option_chain_snapshot: quotes and implied volatility for one "
67
+ "expiry.\n"
68
+ "option_greeks_ladder: sensitivities per contract.\n"
69
+ "option_plots: images for requests to see, show, chart, or plot "
70
+ "option data.\n"
71
+ "option_positioning: walls, gamma flip, max pain, smile geometry.\n"
72
+ "option_strategy_build: one structure.\n"
73
+ "option_strategy_compare: every structure, ranked.\n"
74
+ "option_simulate: forward distribution and tail risk.\n"
75
+ "option_backtest: historical behaviour with modelled premiums.\n"
76
+ "option_forward_test: the paper ledger.\n"
77
+ "option_expiries: what is listed and what is on disk.\n\n"
78
+ "Nothing here answers a question about what someone should do. If "
79
+ "that is the question, say so."),
80
+ ("human", "{question}"),
81
+ ])
@@ -0,0 +1,287 @@
1
+ """The desk's commands, as LangChain tools.
2
+
3
+ Every tool is a thin wrapper over a CLI runner that already exists. The
4
+ wrapper adds no arithmetic and makes no decision: it validates arguments,
5
+ calls the command, and returns the same JSON summary the command prints.
6
+
7
+ Two properties are deliberate.
8
+
9
+ Tools never compute. If a tool did arithmetic of its own, the same question
10
+ asked through the CLI and through an agent would eventually give different
11
+ answers, and there would be no way to tell which was right.
12
+
13
+ Tools return the summary, not the artifact. The summary is small, carries
14
+ the provenance and the degraded flag, and names the artifact path for
15
+ anything that needs the full detail. Returning the whole artifact would
16
+ fill a context window with a chain of six hundred contracts.
17
+ """
18
+
19
+ import inspect
20
+ from typing import Any
21
+
22
+ from optiondesk.cli import backtest as backtest_cmd
23
+ from optiondesk.cli import chain as chain_cmd
24
+ from optiondesk.cli import compare as compare_cmd
25
+ from optiondesk.cli import expiries as expiries_cmd
26
+ from optiondesk.cli import exposure as exposure_cmd
27
+ from optiondesk.cli import forward as forward_cmd
28
+ from optiondesk.cli import greeks as greeks_cmd
29
+ from optiondesk.cli import plots as plots_cmd
30
+ from optiondesk.cli import simulate as simulate_cmd
31
+ from optiondesk.cli import strategy as strategy_cmd
32
+
33
+
34
+ class _Args:
35
+ """A namespace the CLI runners accept, built from declared defaults.
36
+
37
+ Only declared parameters are honoured. An undeclared key is dropped
38
+ rather than set, so a caller cannot reach a runner argument the tool
39
+ does not advertise, such as the output directory.
40
+ """
41
+
42
+ def __init__(self, defaults, supplied):
43
+ self.rejected = []
44
+ for key, value in defaults.items():
45
+ setattr(self, key, value)
46
+ for key, value in (supplied or {}).items():
47
+ if key in defaults:
48
+ setattr(self, key, value)
49
+ else:
50
+ self.rejected.append(key)
51
+
52
+
53
+ def _snapshot_schema(_args):
54
+ return chain_cmd.snapshot_schema()
55
+
56
+
57
+ SPECS = [
58
+ {
59
+ "name": "option_snapshot_schema",
60
+ "description": (
61
+ "Describe the accepted user-supplied CSV and JSON fields, column "
62
+ "aliases, deterministic repairs, limits, and data-rights rule. "
63
+ "Use it to correct an attachment without inventing market values."),
64
+ "runner": _snapshot_schema,
65
+ "defaults": {},
66
+ "public": (),
67
+ },
68
+ {
69
+ "name": "option_chain_snapshot",
70
+ "description": (
71
+ "Retrieve an option chain for one underlying and expiry from a "
72
+ "permitted provider or user-supplied snapshot, solve implied "
73
+ "volatility per contract, and "
74
+ "write a schema validated snapshot. Returns the artifact path "
75
+ "and a summary. Delayed third party data."),
76
+ "runner": chain_cmd.run,
77
+ "defaults": {"symbol": None, "expiry": None, "provider": None,
78
+ "source_path": None, "from_file": None, "rate": None,
79
+ "source_text": None, "source_data": None,
80
+ "source_format": None, "data_source": None,
81
+ "rights_confirmed": False,
82
+ "dividend_yield": None, "out_dir": None},
83
+ "public": ("symbol", "expiry", "source_path", "source_text",
84
+ "source_data", "source_format", "data_source",
85
+ "rights_confirmed", "dividend_yield", "rate"),
86
+ },
87
+ {
88
+ "name": "option_expiries",
89
+ "description": (
90
+ "List the expiries a provider carries for an underlying with "
91
+ "days to expiry, marking which are already on disk. Omit the "
92
+ "symbol to list only what is local, with no network access."),
93
+ "runner": expiries_cmd.run,
94
+ "defaults": {"symbol": None, "provider": None, "out_dir": None},
95
+ "public": ("symbol",),
96
+ },
97
+ {
98
+ "name": "option_greeks_ladder",
99
+ "description": (
100
+ "Compute the full first to third order Greek ladder from a "
101
+ "chain snapshot, using each contract's own implied volatility. "
102
+ "Contracts without a usable volatility are skipped and counted, "
103
+ "never defaulted."),
104
+ "runner": greeks_cmd.run,
105
+ "defaults": {"snapshot": None, "band": 0.10, "type": "both",
106
+ "out_dir": None},
107
+ "public": ("snapshot", "band", "type"),
108
+ },
109
+ {
110
+ "name": "option_plots",
111
+ "description": (
112
+ "Fetch or read a chain and write opaque PNG charts for direct "
113
+ "display: positioning, open interest, volume, implied volatility, "
114
+ "delta, gamma, theta and vega. Use when the user asks to see "
115
+ "plots; return the image paths instead of starting a dashboard."),
116
+ "runner": plots_cmd.run,
117
+ "defaults": {"symbol": None, "expiry": None, "snapshot": None,
118
+ "source_path": None, "rate": None,
119
+ "source_text": None, "source_data": None,
120
+ "source_format": None, "data_source": None,
121
+ "rights_confirmed": False,
122
+ "dividend_yield": None, "band": 0.15,
123
+ "out_dir": None},
124
+ "public": ("symbol", "expiry", "snapshot", "source_path",
125
+ "source_text", "source_data", "source_format",
126
+ "data_source", "rights_confirmed", "rate",
127
+ "dividend_yield", "band"),
128
+ },
129
+ {
130
+ "name": "option_positioning",
131
+ "description": (
132
+ "Dealer gamma exposure by strike, the call and put walls, the "
133
+ "gamma flip levels, max pain, put to call ratios and volatility "
134
+ "smile geometry. Signs rest on an assumption about who holds "
135
+ "what, which the output states."),
136
+ "runner": exposure_cmd.run,
137
+ "defaults": {"snapshot": None, "multiplier": 100.0, "out_dir": None},
138
+ "public": ("snapshot", "multiplier"),
139
+ },
140
+ {
141
+ "name": "option_strategy_build",
142
+ "description": (
143
+ "Build one structure from a chain: iron condor, butterfly, "
144
+ "vertical spreads, straddle, strangle, covered call, cash "
145
+ "secured put, protective put, calendar or diagonal. Pass "
146
+ "list_only to see the playbook, or recommend with an outlook "
147
+ "from -2 to +2 to rank structures for a view."),
148
+ "runner": strategy_cmd.run,
149
+ "defaults": {"name": None, "snapshot": None, "far_snapshot": None,
150
+ "kind": "call", "offset": 0.03, "size": 1.0,
151
+ "underlying_entry": None, "out_dir": None,
152
+ "list_only": False, "recommend": None,
153
+ "vol_view": "neutral", "owns_underlying": False,
154
+ "direction_unknown": False},
155
+ "public": ("name", "snapshot", "far_snapshot", "kind", "size",
156
+ "list_only", "recommend", "vol_view", "owns_underlying",
157
+ "direction_unknown"),
158
+ },
159
+ {
160
+ "name": "option_strategy_compare",
161
+ "description": (
162
+ "Build every structure from one chain and rank them by model "
163
+ "expected profit per unit of capital at risk. Returns the "
164
+ "table, the leader, and the caveat that a positive expectation "
165
+ "largely measures the gap between one volatility and the "
166
+ "market's smile. Not a recommendation."),
167
+ "runner": compare_cmd.run,
168
+ "defaults": {"snapshot": None, "size": 1.0,
169
+ "include_underlying": False, "rebuild": False,
170
+ "out_dir": None},
171
+ "public": ("snapshot", "size", "include_underlying", "rebuild"),
172
+ },
173
+ {
174
+ "name": "option_simulate",
175
+ "description": (
176
+ "Fit a Bayesian GARCH-t model to the underlying's realised "
177
+ "returns by MCMC and simulate forward. Returns the posterior "
178
+ "with convergence diagnostics, the predictive fan, value at "
179
+ "risk and expected shortfall. Check converged before quoting "
180
+ "any quantile."),
181
+ "runner": simulate_cmd.run,
182
+ "defaults": {"symbol": None, "horizon": 5, "paths": 20000,
183
+ "draws": 3000, "burn": 1000, "chains": 2,
184
+ "period": "2y", "provider": None,
185
+ "no_structures": False, "out_dir": None},
186
+ "public": ("symbol", "horizon", "paths", "draws", "period"),
187
+ },
188
+ {
189
+ "name": "option_backtest",
190
+ "description": (
191
+ "Run a structure across real price history with modelled "
192
+ "premiums. Returns win rate, mean return on capital at risk, "
193
+ "drawdown, a permutation test, a bootstrap interval and a buy "
194
+ "and hold benchmark. Premiums are model values, never fills."),
195
+ "runner": backtest_cmd.run,
196
+ "defaults": {"symbol": None, "strategy": None, "holding_days": 30,
197
+ "entry_every": 5, "lookback": 60, "period": "5y",
198
+ "rate": 0.04, "dividend_yield": 0.0, "size": 1.0,
199
+ "provider": None, "out_dir": None},
200
+ "public": ("symbol", "strategy", "holding_days", "entry_every",
201
+ "period"),
202
+ },
203
+ {
204
+ "name": "option_forward_test",
205
+ "description": (
206
+ "Paper ledger of positions recorded before their outcome is "
207
+ "known. Actions: open, mark, close, status. Marks are mid "
208
+ "quotes, not fills, and a position with a leg missing from the "
209
+ "later chain is reported unmarkable rather than marked at "
210
+ "zero."),
211
+ "runner": forward_cmd.run,
212
+ "defaults": {"action": "status", "plan": None, "strategy": None,
213
+ "underlying": None, "position_id": None, "price": None,
214
+ "thesis": None, "out_dir": None},
215
+ "public": ("action", "strategy", "underlying", "position_id",
216
+ "price", "thesis"),
217
+ },
218
+ ]
219
+
220
+
221
+ def tool_specs():
222
+ """The tool definitions, without requiring LangChain to be installed."""
223
+ return [{"name": spec["name"], "description": spec["description"],
224
+ "parameters": list(spec["public"])} for spec in SPECS]
225
+
226
+
227
+ def _annotation_for(default):
228
+ """The type to advertise for a parameter, taken from its own default.
229
+
230
+ A default of None says nothing about the type, so the parameter is
231
+ advertised as Any rather than given a guessed one. Guessing here would
232
+ put a type in the tool schema that no one has checked against the
233
+ command behind it.
234
+ """
235
+ return Any if default is None else type(default)
236
+
237
+
238
+ def _callable_for(spec):
239
+ def run(**kwargs):
240
+ args = _Args(spec["defaults"], kwargs)
241
+ result = spec["runner"](args)
242
+ # Not reachable through desk_tools: the schema published below
243
+ # filters unknown keys before run is called. Kept for direct callers
244
+ # of _callable_for and _Args, which have no schema in front of them.
245
+ if args.rejected:
246
+ result = dict(result)
247
+ result["ignored_arguments"] = args.rejected
248
+ return result
249
+
250
+ # The wrapper takes **kwargs so it can forward anything, but LangChain
251
+ # builds the tool's schema by introspecting the signature. Left bare,
252
+ # **kwargs produces a schema with one opaque object parameter, no
253
+ # parameter is named to the model, and every supplied argument is
254
+ # dropped by validation before run is called. Publishing the declared
255
+ # public parameters is what makes the advertised arguments arrive.
256
+ run.__name__ = spec["name"]
257
+ run.__signature__ = inspect.Signature([
258
+ inspect.Parameter(key, inspect.Parameter.KEYWORD_ONLY,
259
+ default=spec["defaults"][key],
260
+ annotation=_annotation_for(spec["defaults"][key]))
261
+ for key in spec["public"]
262
+ ])
263
+ run.__annotations__ = {
264
+ key: _annotation_for(spec["defaults"][key]) for key in spec["public"]
265
+ }
266
+ return run
267
+
268
+
269
+ def desk_tools(include=None):
270
+ """Build LangChain StructuredTools for the desk.
271
+
272
+ include restricts the set by name, which matters when handing an agent
273
+ a narrower surface than the whole desk: an assistant that should only
274
+ read has no business holding the tool that opens a paper position.
275
+ """
276
+ from langchain_core.tools import StructuredTool
277
+
278
+ tools = []
279
+ for spec in SPECS:
280
+ if include and spec["name"] not in include:
281
+ continue
282
+ tools.append(StructuredTool.from_function(
283
+ func=_callable_for(spec),
284
+ name=spec["name"],
285
+ description=spec["description"],
286
+ ))
287
+ return tools
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.5
2
+ Name: optiondesk-agent
3
+ Version: 0.3.0
4
+ Summary: LangChain bindings for the option desk: tools, artifact retrieval, and a grounded answer chain
5
+ Author: Iman Samizadeh
6
+ License: PolyForm-Noncommercial-1.0.0
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: langchain-core>=0.3
10
+ Requires-Dist: optiondesk
11
+ Provides-Extra: dev
12
+ Requires-Dist: langgraph>=0.2; extra == 'dev'
13
+ Requires-Dist: pytest; extra == 'dev'
14
+ Provides-Extra: graph
15
+ Requires-Dist: langgraph>=0.2; extra == 'graph'
@@ -0,0 +1,9 @@
1
+ optiondesk_agent/__init__.py,sha256=ubik5js8e0f3Vir3oua95qhLXC74vCPO5ZlFb6HtVVI,1206
2
+ optiondesk_agent/artifacts.py,sha256=TXpz0ReKKYucVfxKGVOTqIf_koCFbKpBNWbbQ7oP0CM,12212
3
+ optiondesk_agent/graph.py,sha256=ARyY5DJorKvxADfYsGrVWZiGfGd78NOkNYmMiC5bHQs,14597
4
+ optiondesk_agent/prompts.py,sha256=NCZvw9T1Aiy7xuCpYqrsWCdMr8C3Mg-uw1BHPrL-pPU,3876
5
+ optiondesk_agent/tools.py,sha256=mj2xBinJGSZ6rsJsQcB0VYi1y_EJahMSS3v6UAd4ISQ,12713
6
+ optiondesk_agent-0.3.0.dist-info/METADATA,sha256=LWg7ym-Py90b4YgtS8DYnJ6TjYyQEtAHg8-VotvJixY,506
7
+ optiondesk_agent-0.3.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
8
+ optiondesk_agent-0.3.0.dist-info/licenses/LICENSE,sha256=OxWd1dH5GHkwFMElRHQD0e_AEWvJ-p1E3RolQDB8w3I,4674
9
+ optiondesk_agent-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,76 @@
1
+ # PolyForm Noncommercial License 1.0.0
2
+
3
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
4
+
5
+ Required Notice: Copyright 2026 Iman Samizadeh
6
+ (https://github.com/Iman/agent-driven-options-desk-and-skills)
7
+
8
+ ## Acceptance
9
+
10
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
11
+
12
+ ## Copyright License
13
+
14
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
15
+
16
+ ## Distribution License
17
+
18
+ The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
19
+
20
+ ## Notices
21
+
22
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
23
+
24
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
25
+
26
+ ## Changes and New Works License
27
+
28
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
29
+
30
+ ## Patent License
31
+
32
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
33
+
34
+ ## Noncommercial Purposes
35
+
36
+ Any noncommercial purpose is a permitted purpose.
37
+
38
+ ## Personal Uses
39
+
40
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
41
+
42
+ ## Noncommercial Organizations
43
+
44
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
45
+
46
+ ## Fair Use
47
+
48
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
49
+
50
+ ## No Other Rights
51
+
52
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
53
+
54
+ ## Patent Defense
55
+
56
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
57
+
58
+ ## Violations
59
+
60
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
61
+
62
+ ## No Liability
63
+
64
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
65
+
66
+ ## Definitions
67
+
68
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
69
+
70
+ **You** refers to the individual or entity agreeing to these terms.
71
+
72
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
73
+
74
+ **Your licenses** are all the licenses granted to you for the software under these terms.
75
+
76
+ **Use** means anything you do with the software requiring one of your licenses.