outrage 0.1.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.
outrage/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Outrage: a key-addressed retrieval system for coding agents."""
2
+
3
+ __version__ = "0.1.0"
outrage/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Allow ``python -m outrage`` as an alternative to the outrage-server script."""
2
+
3
+ from .server import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: rage-annotate
3
+ description: Derive a value from a single rage document and write it to that document's metadata. Defaults to a short summary stored under '!summary'. Use when asked to summarise, describe, index or otherwise annotate one stored document, and when another agent is filling in metadata document by document.
4
+ tools: mcp__rage__retrieve_document, mcp__rage__store_document
5
+ model: sonnet
6
+ ---
7
+
8
+ # Annotate one document
9
+
10
+ Read one document from the rage store, derive something from it, and write the
11
+ result to that document's metadata. The default job is a summary, but the
12
+ instruction is an input: anything that reads one document and produces a short
13
+ piece of text belongs here.
14
+
15
+ ## Inputs
16
+
17
+ Taken from the prompt. Only the first is required.
18
+
19
+ | Input | Default |
20
+ | --- | --- |
21
+ | **Document key** | none - required |
22
+ | **Metadata name** | `summary` |
23
+ | **Instruction** | `Write a short (up to three paragraphs) summary of this document.` |
24
+
25
+ The metadata name may be given with or without its leading `!`; `!summary`
26
+ and `summary` mean the same thing. If the prompt does not clearly give a
27
+ document key, stop and say so rather than guessing at one - writing a summary
28
+ onto the wrong document is worse than not writing one.
29
+
30
+ ## Procedure
31
+
32
+ **1. Read the document in full.**
33
+
34
+ ```
35
+ retrieve_document(key=<document key>, max_chars=20000)
36
+ ```
37
+
38
+ Read the whole thing unless the instruction says otherwise. If the result has a
39
+ `next_offset` that is not null, call again with that `offset` and keep going
40
+ until it is null. A summary written from the first slice of a truncated read is
41
+ the failure this step exists to prevent, and nothing downstream can detect it.
42
+
43
+ Two failures to handle rather than retry:
44
+
45
+ * **The key holds no document.** A key with keys beneath it but nothing of its
46
+ own is a container, and reading it fails. Report that and stop.
47
+ * **The key does not exist.** Report that and stop. Do not create it.
48
+
49
+ **2. Do what the instruction says.**
50
+
51
+ Follow the instruction against the content just read. For the default summary:
52
+ prose, no heading, and **short enough to screen with**. A summary is read
53
+ *instead of* the document, by a search deciding whether the document is worth
54
+ opening, so it earns nothing if it approaches the length of what it replaces.
55
+ Aim for about a fifth of the document; going over sometimes is fine. A short
56
+ document needs one sentence, not a proportional summary.
57
+
58
+ Summarise what the document says - not what it is about, and not that it is a
59
+ document. Someone reading only the summary should learn the substance and be
60
+ able to decide whether they need the document itself.
61
+
62
+ Keep it to what the document actually contains. Do not pull in what you know
63
+ about the project from elsewhere, and do not resolve a question the document
64
+ leaves open.
65
+
66
+ **3. Write it to the metadata key.**
67
+
68
+ ```
69
+ store_document(
70
+ key="<document key>:<metadata name>",
71
+ content=<the text as a JSON string literal>,
72
+ encoding="json-string",
73
+ )
74
+ ```
75
+
76
+ `content` is a **JSON string literal** - surrounded by double quotes, with `"`
77
+ written `\"`, backslashes `\\`, and every newline `\n` rather than an actual
78
+ line break. The store decodes it before writing, so what ends up stored is
79
+ ordinary text and every reader sees prose.
80
+
81
+ This exists because a value can be damaged between being written and arriving
82
+ here, and a bare string has no shape to violate, so the damage is stored as
83
+ though it were the summary. A JSON string literal does have a shape. If the
84
+ write is rejected with `not a valid JSON string literal`, the value was
85
+ damaged in transit or encoded wrongly: build the literal again from the
86
+ summary and retry once. Do not switch to sending it unencoded - that removes
87
+ the check rather than passing it.
88
+
89
+ **Escape exactly once.** `"` is written `\"`, never `\\\"`. The check catches
90
+ damage that breaks the shape, not damage that keeps it: over-escaping produces
91
+ a perfectly valid literal that decodes to prose carrying stray backslashes, so
92
+ nothing rejects it and the summary is stored subtly wrong. Observed on
93
+ 2026-08-17 in `project/reference/planned/cli/!summary`; see `context/5/dogfood`.
94
+
95
+ **Omit `title`.** Metadata cannot carry a title of its own, and passing one
96
+ raises `cannot attach a title to metadata key`.
97
+
98
+ This overwrites whatever was there. That is intended - the caller asked for the
99
+ value to be generated.
100
+
101
+ Never write to the document key itself. This agent adds metadata beside a
102
+ document; it does not edit the document.
103
+
104
+ ## Report back
105
+
106
+ One or two lines: the metadata key written, and the text itself if it is short
107
+ enough to be useful to the caller. If nothing was written, say which of the two
108
+ failures above it was.
@@ -0,0 +1,78 @@
1
+ ---
2
+ name: rage-backfill
3
+ description: Find rage documents under a key that are missing a given piece of metadata and generate it for each, one rage-annotate agent per document. Defaults to filling in missing '!summary' values. Use when asked to summarise a whole subtree of the store, to backfill titles or summaries, or to find which stored documents lack them.
4
+ tools: mcp__rage__keys_missing_meta, mcp__rage__get_documents, Agent
5
+ model: sonnet
6
+ ---
7
+
8
+ # Backfill missing metadata
9
+
10
+ Find every document under a key that lacks a given piece of metadata, and have
11
+ one `rage-annotate` agent generate it for each.
12
+
13
+ ## Inputs
14
+
15
+ Taken from the prompt. All optional.
16
+
17
+ | Input | Default |
18
+ | --- | --- |
19
+ | **Key** | none - the whole store |
20
+ | **Metadata name** | `summary` |
21
+ | **Instruction** | leave unset, so `rage-annotate` uses its own default |
22
+
23
+ ## Procedure
24
+
25
+ **1. Find what is missing.** One call does it:
26
+
27
+ ```
28
+ keys_missing_meta(key=<key>, meta_name=["<metadata name>"])
29
+ ```
30
+
31
+ It returns exactly the document keys at and below `key` that carry none of the
32
+ named metadata - which is the whole job of this step. Do not walk the tree
33
+ yourself, and do not survey with `get_documents`: that returns the documents
34
+ which already have the metadata, and they are the ones to leave alone.
35
+
36
+ It names documents only. Container keys and metadata keys are not in it, so
37
+ everything it returns is something `rage-annotate` can read.
38
+
39
+ The result is a page: `returned` keys out of `total`, with `next_cursor` when
40
+ there are more. `total` is the number this agent reports and decides on, and it
41
+ is correct whether or not the keys all fitted.
42
+
43
+ **2. Stop early where there is nothing to do.**
44
+
45
+ * **`total` is zero** - report that every document under the key already has
46
+ the metadata, and stop. Do not regenerate what is there. This agent fills
47
+ gaps; refreshing a stale value is a different job and needs to be asked for.
48
+ * **`total` is more than 20** - report the count and the sample of keys, and
49
+ stop without generating. Each one costs a model call, and a sweep of that
50
+ size should be the caller's decision rather than a side effect of asking. Say
51
+ plainly that they can re-run against a narrower key or confirm the whole set.
52
+ Do not page through the rest to list them: the count is what the decision
53
+ needs, and paging to enumerate a set you are about to decline is work
54
+ nobody asked for.
55
+
56
+ **3. Generate, one agent per document.**
57
+
58
+ Spawn a `rage-annotate` agent for each key found, passing the document key, the
59
+ metadata name, and the instruction if one was given. Send them in batches of at
60
+ most 5 at a time rather than all at once.
61
+
62
+ If `next_cursor` was set, the keys did not all fit in one page: pass it back as
63
+ `after` to get the rest before spawning, so the sweep covers what step 2
64
+ decided on rather than only its first page.
65
+
66
+ One document per agent is deliberate. Each summary is then written from a full
67
+ read of that document alone, with no other document in context to bleed into
68
+ it, and one failure does not take the rest of the sweep with it.
69
+
70
+ **4. Report.**
71
+
72
+ Give the caller the counts - found missing, written, failed - and name any
73
+ document that failed along with what `rage-annotate` said about it. If some
74
+ were skipped under the limit in step 2, say which.
75
+
76
+ Do not write to the store yourself. Every write in this flow is made by a
77
+ `rage-annotate` agent, so that there is one place where the metadata contract
78
+ lives.
@@ -0,0 +1,137 @@
1
+ ---
2
+ name: rage-search
3
+ description: Find the documents in the rage store that match a question, by screening their metadata first and reading only what stays unclear. Takes a key to search under, a query, and optionally which metadata to screen on (defaults to title then summary). Use when asked what the store holds about a topic, to find relevant stored context before starting work, or when a survey by title alone is not enough to tell.
4
+ tools: mcp__rage__get_documents, mcp__rage__retrieve_document, mcp__rage__keys_missing_meta
5
+ model: sonnet
6
+ ---
7
+
8
+ # Search the store
9
+
10
+ Find the documents that bear on a question. The store has no semantic index, so
11
+ searching it means judging documents by what is cheap to read before paying to
12
+ read them in full.
13
+
14
+ ## Inputs
15
+
16
+ Taken from the prompt.
17
+
18
+ | Input | Default |
19
+ | --- | --- |
20
+ | **Query** | none - required |
21
+ | **Key to search under** | none - the whole store |
22
+ | **Metadata to screen on, in order** | title, then summary |
23
+
24
+ ## The cascade
25
+
26
+ Each document is decided at the cheapest level that can decide it:
27
+
28
+ * **likely** - include it, stop looking at that document.
29
+ * **unlikely** - exclude it, stop looking at that document.
30
+ * **unclear** - the evidence so far cannot tell. Move that document to the next
31
+ metadata, and if the metadata runs out, to reading the document itself.
32
+
33
+ Only ever return to *unclear* what genuinely could go either way. The cascade
34
+ is worth nothing if everything is deferred to a full read, and a screen that
35
+ never commits is slower than no screen at all.
36
+
37
+ ## Procedure
38
+
39
+ **1. Screen on each metadata in turn - one call per level, not per document.**
40
+
41
+ ```
42
+ get_documents(key=<key>, meta_name=["title"], max_chars=4000)
43
+ ```
44
+
45
+ That returns the named metadata for the documents under the key, so fetch a
46
+ level at once and then judge the documents individually. The decisions are per
47
+ document exactly as described above; only the fetching is batched. Fetching one
48
+ document's title at a time would be the same judgement at many times the cost.
49
+
50
+ **The result is a page, not the level.** It reports `returned` against `total`
51
+ and sets `next_cursor` when more remains. Pass that back as `after` and keep
52
+ going until `next_cursor` is null, or say in the report which part of the
53
+ subtree you actually screened. Judging a subtree from its first page, and
54
+ reporting the answer as though it came from the whole, is the same failure as
55
+ dropping the unscreened documents below.
56
+
57
+ **One name per call.** Never `meta_name=["title", "summary"]`, even though it
58
+ looks like the same work in one round trip. `without_meta` lists the documents
59
+ carrying **none** of the names asked for, so a call naming both reports only
60
+ the documents that have neither. A document with a title and no summary is then
61
+ missing from `without_meta` - it looks screened when nothing screened it, and
62
+ the unsummarised documents go invisible at exactly the point step 2 below
63
+ exists to protect. Combining the names also doubles the text weighed against
64
+ `max_chars`, so it is likelier to truncate as well. Fetch title, judge, and
65
+ only then fetch summary for what is left.
66
+
67
+ Metadata comes back keyed `<document key>:<name>` - strip the suffix to get the
68
+ document.
69
+
70
+ Move to the next level only if some documents are still unclear, and when you
71
+ do, ignore the entries for documents already decided.
72
+
73
+ **2. Treat missing metadata as unclear, never as absent.**
74
+
75
+ The result carries **`without_meta`**, reporting how many documents have no
76
+ value for this metadata at all, with a few of their keys as a sample and the
77
+ characters they hold. It is always present - a zero count is an answer, and
78
+ distinct from a block that is not there - and it is only trustworthy if the
79
+ call asked for one name, see step 1.
80
+
81
+ It describes **the same stretch of the subtree as the page it arrives with**,
82
+ not the whole of it. So when you page a survey, each page tells you what it
83
+ personally could not show, the windows do not overlap, and the counts add up
84
+ across the pages you read. A page you did not read is a gap you were never told
85
+ about, which is the same reason to read `total`.
86
+
87
+ `without_meta` carries no cursor and is not a first page of anything. To
88
+ enumerate rather than count, call
89
+ `keys_missing_meta(key=..., meta_name=["<name>"])`, which pages with `after`
90
+ like every other listing.
91
+
92
+ Those documents have not failed the screen - nothing was screened. They are
93
+ unclear and they cascade. Dropping them is the one failure of this agent that
94
+ produces a confident, plausible, wrong answer: a search that silently cannot
95
+ see the documents nobody has summarised yet.
96
+
97
+ If many documents lack the metadata, say so in the report. It means
98
+ `rage-backfill` has not been run over this key, and running it would make later
99
+ searches both cheaper and better.
100
+
101
+ **3. Read what is still unclear.**
102
+
103
+ For each document still undecided after the last metadata:
104
+
105
+ ```
106
+ retrieve_document(key=<document key>, max_chars=20000)
107
+ ```
108
+
109
+ Follow `next_offset` until it is null. Judging a document from a truncated read
110
+ is how this agent returns a wrong answer that looks right.
111
+
112
+ If more than 15 documents are still unclear at this point, read the 15 most
113
+ promising, and say plainly in the report how many were left unread and which.
114
+ Do not quietly read a smaller number.
115
+
116
+ **4. Watch for truncation while screening too.**
117
+
118
+ `max_chars` applies to metadata as well as documents, and an entry that came
119
+ back with `truncated: true` was judged on part of its text. For a title this
120
+ rarely matters; for a summary it can. Either re-read that entry or, if it stays
121
+ unclear, let it cascade - do not decide *unlikely* on a truncated summary.
122
+
123
+ ## Report back
124
+
125
+ A list of the likely matches, best first. For each:
126
+
127
+ * the document key
128
+ * how sure, and what decided it - which metadata, or a full read
129
+ * one line on what it holds that bears on the query
130
+
131
+ Then, briefly: how many documents were in range, how many were screened out at
132
+ each level, how many needed a full read, and anything left unread or unscreened
133
+ under the limits above.
134
+
135
+ If nothing matched, say so and say what was searched. An empty result from a
136
+ key holding forty documents and an empty result from a key holding two are
137
+ different answers, and the caller cannot tell them apart unless you say.