kredisco 0.1.0__tar.gz
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.
- kredisco-0.1.0/PKG-INFO +324 -0
- kredisco-0.1.0/README.md +310 -0
- kredisco-0.1.0/kredisco/__init__.py +5 -0
- kredisco-0.1.0/kredisco/client.py +238 -0
- kredisco-0.1.0/kredisco/identity.py +76 -0
- kredisco-0.1.0/kredisco/receipt.py +57 -0
- kredisco-0.1.0/kredisco.egg-info/PKG-INFO +324 -0
- kredisco-0.1.0/kredisco.egg-info/SOURCES.txt +11 -0
- kredisco-0.1.0/kredisco.egg-info/dependency_links.txt +1 -0
- kredisco-0.1.0/kredisco.egg-info/requires.txt +2 -0
- kredisco-0.1.0/kredisco.egg-info/top_level.txt +1 -0
- kredisco-0.1.0/pyproject.toml +24 -0
- kredisco-0.1.0/setup.cfg +4 -0
kredisco-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kredisco
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A trust layer for AI agents
|
|
5
|
+
Author: Rahul Sah
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://kredisco.com
|
|
8
|
+
Project-URL: Source, https://github.com/kredisco/Kredisco
|
|
9
|
+
Keywords: ai,agents,trust,reputation,llm,langgraph
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: requests>=2.28
|
|
13
|
+
Requires-Dist: cryptography>=41.0
|
|
14
|
+
|
|
15
|
+
# Kredisco
|
|
16
|
+
|
|
17
|
+
**A trust layer for AI agents.**
|
|
18
|
+
|
|
19
|
+
Humans carry a credit score. It follows you between lenders, it is held
|
|
20
|
+
by a bureau rather than by you, and it is built from what other parties
|
|
21
|
+
report about your behaviour. A landlord who has never met you can decide
|
|
22
|
+
whether to trust you in seconds.
|
|
23
|
+
|
|
24
|
+
AI agents have no equivalent. You wire one into your pipeline and have
|
|
25
|
+
no idea whether it is dependable until something breaks.
|
|
26
|
+
|
|
27
|
+
Kredisco gives every agent a portable trust score, 300 to 850, earned
|
|
28
|
+
from its real track record and held where the agent cannot reach it.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Why agents need one
|
|
33
|
+
|
|
34
|
+
A pipeline with six agents fails quietly. One step degrades, output
|
|
35
|
+
quality drops, and you find out from a user complaint days later.
|
|
36
|
+
Nothing tells you which agent to stop calling.
|
|
37
|
+
|
|
38
|
+
The instinct is to ask the agents, or to collect ratings. Neither
|
|
39
|
+
survives contact with incentives. An agent will always report that it
|
|
40
|
+
did well. Open rating systems get farmed: an audit of ERC-8004's on-chain
|
|
41
|
+
reputation registry found the large majority of reviewers exhibited
|
|
42
|
+
coordinated Sybil behaviour, and after removing them most rated agents
|
|
43
|
+
had no valid feedback left.
|
|
44
|
+
|
|
45
|
+
Credit scoring solved this problem a long time ago, and not with better
|
|
46
|
+
surveys. It solved it structurally: **the party being scored is never
|
|
47
|
+
the party reporting.** Your bank reports your payments. You cannot file
|
|
48
|
+
your own. You cannot edit the file. You cannot see the number until
|
|
49
|
+
someone pulls it.
|
|
50
|
+
|
|
51
|
+
Kredisco applies that structure to agents.
|
|
52
|
+
|
|
53
|
+
## What the score is made of
|
|
54
|
+
|
|
55
|
+
Like FICO, the score is a weighted blend of factors, and like FICO,
|
|
56
|
+
every one of them comes from somewhere the scored party cannot reach.
|
|
57
|
+
|
|
58
|
+
| Factor | Weight | Where it comes from |
|
|
59
|
+
|---|---:|---|
|
|
60
|
+
| Rework rate (inverted) | 0.45 | You retried, reassigned, or the next step errored |
|
|
61
|
+
| On-time rate | 0.20 | Measured by the SDK wrapping the call |
|
|
62
|
+
| Counterparty diversity | 0.20 | How many distinct organizations hired it |
|
|
63
|
+
| History depth | 0.15 | Volume of recorded work |
|
|
64
|
+
|
|
65
|
+
Scores run 300–850. A new agent scores 300, so abandoning a damaged
|
|
66
|
+
identity means abandoning everything it earned.
|
|
67
|
+
|
|
68
|
+
Rework carries the most weight because it is hardest to fake. An agent
|
|
69
|
+
cannot retry itself, cannot stop a downstream step from failing, and
|
|
70
|
+
cannot stop you from routing around it.
|
|
71
|
+
|
|
72
|
+
## Who reports, and why it can't be gamed
|
|
73
|
+
|
|
74
|
+
A lender reports your payment to the bureau. The bureau holds the file.
|
|
75
|
+
A future lender queries the bureau, never you.
|
|
76
|
+
|
|
77
|
+
Kredisco works the same way. Your orchestrator is the reporter, Kredisco
|
|
78
|
+
is the bureau, and the agent is the subject:
|
|
79
|
+
|
|
80
|
+
1. Your agent completes a task
|
|
81
|
+
2. The agent signs a record of it; your orchestrator countersigns
|
|
82
|
+
3. Your orchestrator submits it, authenticated with your API key
|
|
83
|
+
4. Kredisco verifies both signatures and files it under your organization
|
|
84
|
+
5. The score is recomputed from everything on file
|
|
85
|
+
|
|
86
|
+
Both signatures are required, so neither side can act alone. An agent
|
|
87
|
+
cannot manufacture a history, because every entry needs a real
|
|
88
|
+
counterparty to have signed it. And it never holds its own score — when
|
|
89
|
+
someone wants to know whether to trust it, they ask Kredisco.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Quickstart
|
|
94
|
+
|
|
95
|
+
### 1. Get a key
|
|
96
|
+
|
|
97
|
+
Sign in with GitHub at the Kredisco dashboard and create an API key. One
|
|
98
|
+
key covers everything you build.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
export KREDISCO_API_KEY=kd_...
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 2. Install
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pip install kredisco
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 3. Connect
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
import os
|
|
114
|
+
from kredisco import Kredisco
|
|
115
|
+
|
|
116
|
+
kd = Kredisco(api_key=os.environ["KREDISCO_API_KEY"])
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 4. Name each agent you want scored
|
|
120
|
+
|
|
121
|
+
An "agent" is any component you call and could imagine replacing: a
|
|
122
|
+
model behind a prompt, a third-party API, a tool, a subprocess. Give it
|
|
123
|
+
a name once. Kredisco creates its identity and reuses it forever after.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
extractor = kd.agent("invoice-extractor")
|
|
127
|
+
reviewer = kd.agent("code-reviewer")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 5. Route your calls through `track`
|
|
131
|
+
|
|
132
|
+
Take the call you already make:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
data = extract_invoice(pdf)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Hand the function to `track` instead of calling it yourself:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
data = kd.track(extractor, "extract", extract_invoice, pdf)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Four arguments: the agent, a label for the kind of work, **the function
|
|
145
|
+
without parentheses**, then its arguments.
|
|
146
|
+
|
|
147
|
+
Kredisco calls the function, times it, files a record, and hands back
|
|
148
|
+
exactly what your function returned. If it raises, the failure is
|
|
149
|
+
recorded and the exception propagates normally. Your logic does not
|
|
150
|
+
change.
|
|
151
|
+
|
|
152
|
+
That is the integration. Scores start appearing on the dashboard.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Telling Kredisco what counts as a failure
|
|
157
|
+
|
|
158
|
+
By default a task fails only if your function raises.
|
|
159
|
+
|
|
160
|
+
Most pipelines have a stricter rule — a required field, a schema, a
|
|
161
|
+
minimum length. Pass `validate` and Kredisco uses yours:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
data = kd.track(
|
|
165
|
+
extractor, "extract", extract_invoice, pdf,
|
|
166
|
+
validate=lambda d: d is not None and "total" in d,
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**This matters more than it looks.** With no validator and nothing
|
|
171
|
+
raising, every task passes, and the largest factor in the score is
|
|
172
|
+
measuring nothing. Kredisco is worth most to pipelines that already
|
|
173
|
+
check their own output.
|
|
174
|
+
|
|
175
|
+
## Retries
|
|
176
|
+
|
|
177
|
+
Add `retries=1` and a failed attempt runs again. Kredisco links the
|
|
178
|
+
retry to the original task, and that link is what drives the rework
|
|
179
|
+
factor:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
data = kd.track(
|
|
183
|
+
extractor, "extract", extract_invoice, pdf,
|
|
184
|
+
validate=lambda d: d is not None and "total" in d,
|
|
185
|
+
retries=1,
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
If your orchestrator already retries on its own, leave this off and use
|
|
190
|
+
the context manager below for each attempt.
|
|
191
|
+
|
|
192
|
+
## When everything fails
|
|
193
|
+
|
|
194
|
+
If every attempt fails, `track` re-raises the last exception. Sometimes
|
|
195
|
+
that is what you want. Often it is not — a graph node that stops the
|
|
196
|
+
whole run because one step returned bad JSON is worse than a node that
|
|
197
|
+
carries on with an empty value.
|
|
198
|
+
|
|
199
|
+
Pass `default` and `track` returns it instead of raising:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
data = kd.track(
|
|
203
|
+
extractor, "extract", extract_invoice, pdf,
|
|
204
|
+
validate=lambda d: d is not None and "total" in d,
|
|
205
|
+
retries=1,
|
|
206
|
+
default={},
|
|
207
|
+
)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The failure is still recorded and the score still drops. Only your
|
|
211
|
+
control flow changes.
|
|
212
|
+
|
|
213
|
+
Without `default`, every call site needs its own `try`/`except`. With
|
|
214
|
+
it, a pipeline step is one expression:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
def extract_node(state):
|
|
218
|
+
return {"fields": kd.track(
|
|
219
|
+
extractor, "extract", extract_invoice, state["pdf"],
|
|
220
|
+
validate=fields_ok, retries=1, default={},
|
|
221
|
+
)}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## When you cannot hand over a function
|
|
225
|
+
|
|
226
|
+
Some code will not collapse into a single call — the body of a graph
|
|
227
|
+
node, a streamed response consumed in pieces, a block with setup and
|
|
228
|
+
teardown around it. Open a task and report the outcome yourself:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
with kd.task(reviewer, "review") as t:
|
|
232
|
+
findings = run_review(diff)
|
|
233
|
+
t.accepted = len(findings) > 0
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The clock starts when the block opens and stops when it closes. An
|
|
237
|
+
exception inside records a failure and re-raises.
|
|
238
|
+
|
|
239
|
+
## Grouping by pipeline
|
|
240
|
+
|
|
241
|
+
If you run more than one pipeline, tag each one. Same key, different
|
|
242
|
+
label — they appear as separate groups on the dashboard but roll up to
|
|
243
|
+
one organization.
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
kd = Kredisco(
|
|
247
|
+
api_key=os.environ["KREDISCO_API_KEY"],
|
|
248
|
+
workflow_id="invoice-ingest",
|
|
249
|
+
)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Reading scores
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
kd.score(extractor.pubkey) # one number, 300–850
|
|
256
|
+
kd.breakdown(extractor.pubkey) # what the score is made of
|
|
257
|
+
kd.dashboard() # your agents, grouped by pipeline
|
|
258
|
+
kd.best("extract", minimum=650) # highest scorer for a kind of work
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`best` is the point of the whole thing: pick who does the work by track
|
|
262
|
+
record instead of hardcoding a vendor and hoping.
|
|
263
|
+
|
|
264
|
+
## Things worth knowing
|
|
265
|
+
|
|
266
|
+
**Reporting never breaks your pipeline.** If Kredisco is unreachable or
|
|
267
|
+
your key is rejected, the failure is logged and your work continues.
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
import logging
|
|
271
|
+
logging.getLogger("kredisco").setLevel(logging.INFO)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Agent keys live on disk.** Kredisco writes each agent's keypair to
|
|
275
|
+
`.kredisco/`. That directory *is* your agents' identity — lose it and
|
|
276
|
+
every score resets to 300. Back it up, and add it to `.gitignore`.
|
|
277
|
+
|
|
278
|
+
**Timestamps come from the SDK**, not from your agent, so an agent
|
|
279
|
+
cannot shorten its own recorded duration.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## A working example
|
|
286
|
+
|
|
287
|
+
[`examples/langgraph_support_triage.py`](examples/langgraph_support_triage.py)
|
|
288
|
+
is a four-agent LangGraph pipeline built on two different Claude models,
|
|
289
|
+
with a validator on every step.
|
|
290
|
+
|
|
291
|
+
Run it a few times. The scores separate for real reasons — one agent
|
|
292
|
+
returns JSON wrapped in code fences, another hits a response shape the
|
|
293
|
+
caller did not expect, a third is simply slower. Fix the prompt that is
|
|
294
|
+
failing and watch that agent climb over subsequent runs, without ever
|
|
295
|
+
catching the agent that never failed.
|
|
296
|
+
|
|
297
|
+
That gap is the point. History has weight, and recovery takes work.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## What this does not solve
|
|
302
|
+
|
|
303
|
+
**A dishonest reporter can fabricate everything.** Signatures prove who
|
|
304
|
+
signed, not that the content is true. Credit bureaus have the same hole
|
|
305
|
+
and handle it with licensing and liability rather than cryptography.
|
|
306
|
+
Kredisco's mitigation is that keys are tied to GitHub accounts and the
|
|
307
|
+
diversity factor requires many distinct organizations.
|
|
308
|
+
|
|
309
|
+
**Identity is still cheap.** A bad score can be abandoned for a fresh
|
|
310
|
+
keypair. Starting at the floor makes that costly, not impossible.
|
|
311
|
+
|
|
312
|
+
**One score across all task types.** An agent good at summarising and
|
|
313
|
+
bad at code review averages into noise.
|
|
314
|
+
|
|
315
|
+
**Rate limiting is per-process** and does not survive horizontal
|
|
316
|
+
scaling.
|
|
317
|
+
|
|
318
|
+
**No test suite.** The scoring model has changed several times and was
|
|
319
|
+
verified by inspection.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
[](https://www.linkedin.com/in/reachrshah)
|
|
324
|
+
[](https://github.com/idfwyy)
|
kredisco-0.1.0/README.md
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# Kredisco
|
|
2
|
+
|
|
3
|
+
**A trust layer for AI agents.**
|
|
4
|
+
|
|
5
|
+
Humans carry a credit score. It follows you between lenders, it is held
|
|
6
|
+
by a bureau rather than by you, and it is built from what other parties
|
|
7
|
+
report about your behaviour. A landlord who has never met you can decide
|
|
8
|
+
whether to trust you in seconds.
|
|
9
|
+
|
|
10
|
+
AI agents have no equivalent. You wire one into your pipeline and have
|
|
11
|
+
no idea whether it is dependable until something breaks.
|
|
12
|
+
|
|
13
|
+
Kredisco gives every agent a portable trust score, 300 to 850, earned
|
|
14
|
+
from its real track record and held where the agent cannot reach it.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Why agents need one
|
|
19
|
+
|
|
20
|
+
A pipeline with six agents fails quietly. One step degrades, output
|
|
21
|
+
quality drops, and you find out from a user complaint days later.
|
|
22
|
+
Nothing tells you which agent to stop calling.
|
|
23
|
+
|
|
24
|
+
The instinct is to ask the agents, or to collect ratings. Neither
|
|
25
|
+
survives contact with incentives. An agent will always report that it
|
|
26
|
+
did well. Open rating systems get farmed: an audit of ERC-8004's on-chain
|
|
27
|
+
reputation registry found the large majority of reviewers exhibited
|
|
28
|
+
coordinated Sybil behaviour, and after removing them most rated agents
|
|
29
|
+
had no valid feedback left.
|
|
30
|
+
|
|
31
|
+
Credit scoring solved this problem a long time ago, and not with better
|
|
32
|
+
surveys. It solved it structurally: **the party being scored is never
|
|
33
|
+
the party reporting.** Your bank reports your payments. You cannot file
|
|
34
|
+
your own. You cannot edit the file. You cannot see the number until
|
|
35
|
+
someone pulls it.
|
|
36
|
+
|
|
37
|
+
Kredisco applies that structure to agents.
|
|
38
|
+
|
|
39
|
+
## What the score is made of
|
|
40
|
+
|
|
41
|
+
Like FICO, the score is a weighted blend of factors, and like FICO,
|
|
42
|
+
every one of them comes from somewhere the scored party cannot reach.
|
|
43
|
+
|
|
44
|
+
| Factor | Weight | Where it comes from |
|
|
45
|
+
|---|---:|---|
|
|
46
|
+
| Rework rate (inverted) | 0.45 | You retried, reassigned, or the next step errored |
|
|
47
|
+
| On-time rate | 0.20 | Measured by the SDK wrapping the call |
|
|
48
|
+
| Counterparty diversity | 0.20 | How many distinct organizations hired it |
|
|
49
|
+
| History depth | 0.15 | Volume of recorded work |
|
|
50
|
+
|
|
51
|
+
Scores run 300–850. A new agent scores 300, so abandoning a damaged
|
|
52
|
+
identity means abandoning everything it earned.
|
|
53
|
+
|
|
54
|
+
Rework carries the most weight because it is hardest to fake. An agent
|
|
55
|
+
cannot retry itself, cannot stop a downstream step from failing, and
|
|
56
|
+
cannot stop you from routing around it.
|
|
57
|
+
|
|
58
|
+
## Who reports, and why it can't be gamed
|
|
59
|
+
|
|
60
|
+
A lender reports your payment to the bureau. The bureau holds the file.
|
|
61
|
+
A future lender queries the bureau, never you.
|
|
62
|
+
|
|
63
|
+
Kredisco works the same way. Your orchestrator is the reporter, Kredisco
|
|
64
|
+
is the bureau, and the agent is the subject:
|
|
65
|
+
|
|
66
|
+
1. Your agent completes a task
|
|
67
|
+
2. The agent signs a record of it; your orchestrator countersigns
|
|
68
|
+
3. Your orchestrator submits it, authenticated with your API key
|
|
69
|
+
4. Kredisco verifies both signatures and files it under your organization
|
|
70
|
+
5. The score is recomputed from everything on file
|
|
71
|
+
|
|
72
|
+
Both signatures are required, so neither side can act alone. An agent
|
|
73
|
+
cannot manufacture a history, because every entry needs a real
|
|
74
|
+
counterparty to have signed it. And it never holds its own score — when
|
|
75
|
+
someone wants to know whether to trust it, they ask Kredisco.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Quickstart
|
|
80
|
+
|
|
81
|
+
### 1. Get a key
|
|
82
|
+
|
|
83
|
+
Sign in with GitHub at the Kredisco dashboard and create an API key. One
|
|
84
|
+
key covers everything you build.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
export KREDISCO_API_KEY=kd_...
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 2. Install
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install kredisco
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 3. Connect
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import os
|
|
100
|
+
from kredisco import Kredisco
|
|
101
|
+
|
|
102
|
+
kd = Kredisco(api_key=os.environ["KREDISCO_API_KEY"])
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 4. Name each agent you want scored
|
|
106
|
+
|
|
107
|
+
An "agent" is any component you call and could imagine replacing: a
|
|
108
|
+
model behind a prompt, a third-party API, a tool, a subprocess. Give it
|
|
109
|
+
a name once. Kredisco creates its identity and reuses it forever after.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
extractor = kd.agent("invoice-extractor")
|
|
113
|
+
reviewer = kd.agent("code-reviewer")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 5. Route your calls through `track`
|
|
117
|
+
|
|
118
|
+
Take the call you already make:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
data = extract_invoice(pdf)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Hand the function to `track` instead of calling it yourself:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
data = kd.track(extractor, "extract", extract_invoice, pdf)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Four arguments: the agent, a label for the kind of work, **the function
|
|
131
|
+
without parentheses**, then its arguments.
|
|
132
|
+
|
|
133
|
+
Kredisco calls the function, times it, files a record, and hands back
|
|
134
|
+
exactly what your function returned. If it raises, the failure is
|
|
135
|
+
recorded and the exception propagates normally. Your logic does not
|
|
136
|
+
change.
|
|
137
|
+
|
|
138
|
+
That is the integration. Scores start appearing on the dashboard.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Telling Kredisco what counts as a failure
|
|
143
|
+
|
|
144
|
+
By default a task fails only if your function raises.
|
|
145
|
+
|
|
146
|
+
Most pipelines have a stricter rule — a required field, a schema, a
|
|
147
|
+
minimum length. Pass `validate` and Kredisco uses yours:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
data = kd.track(
|
|
151
|
+
extractor, "extract", extract_invoice, pdf,
|
|
152
|
+
validate=lambda d: d is not None and "total" in d,
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**This matters more than it looks.** With no validator and nothing
|
|
157
|
+
raising, every task passes, and the largest factor in the score is
|
|
158
|
+
measuring nothing. Kredisco is worth most to pipelines that already
|
|
159
|
+
check their own output.
|
|
160
|
+
|
|
161
|
+
## Retries
|
|
162
|
+
|
|
163
|
+
Add `retries=1` and a failed attempt runs again. Kredisco links the
|
|
164
|
+
retry to the original task, and that link is what drives the rework
|
|
165
|
+
factor:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
data = kd.track(
|
|
169
|
+
extractor, "extract", extract_invoice, pdf,
|
|
170
|
+
validate=lambda d: d is not None and "total" in d,
|
|
171
|
+
retries=1,
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
If your orchestrator already retries on its own, leave this off and use
|
|
176
|
+
the context manager below for each attempt.
|
|
177
|
+
|
|
178
|
+
## When everything fails
|
|
179
|
+
|
|
180
|
+
If every attempt fails, `track` re-raises the last exception. Sometimes
|
|
181
|
+
that is what you want. Often it is not — a graph node that stops the
|
|
182
|
+
whole run because one step returned bad JSON is worse than a node that
|
|
183
|
+
carries on with an empty value.
|
|
184
|
+
|
|
185
|
+
Pass `default` and `track` returns it instead of raising:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
data = kd.track(
|
|
189
|
+
extractor, "extract", extract_invoice, pdf,
|
|
190
|
+
validate=lambda d: d is not None and "total" in d,
|
|
191
|
+
retries=1,
|
|
192
|
+
default={},
|
|
193
|
+
)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The failure is still recorded and the score still drops. Only your
|
|
197
|
+
control flow changes.
|
|
198
|
+
|
|
199
|
+
Without `default`, every call site needs its own `try`/`except`. With
|
|
200
|
+
it, a pipeline step is one expression:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
def extract_node(state):
|
|
204
|
+
return {"fields": kd.track(
|
|
205
|
+
extractor, "extract", extract_invoice, state["pdf"],
|
|
206
|
+
validate=fields_ok, retries=1, default={},
|
|
207
|
+
)}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## When you cannot hand over a function
|
|
211
|
+
|
|
212
|
+
Some code will not collapse into a single call — the body of a graph
|
|
213
|
+
node, a streamed response consumed in pieces, a block with setup and
|
|
214
|
+
teardown around it. Open a task and report the outcome yourself:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
with kd.task(reviewer, "review") as t:
|
|
218
|
+
findings = run_review(diff)
|
|
219
|
+
t.accepted = len(findings) > 0
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The clock starts when the block opens and stops when it closes. An
|
|
223
|
+
exception inside records a failure and re-raises.
|
|
224
|
+
|
|
225
|
+
## Grouping by pipeline
|
|
226
|
+
|
|
227
|
+
If you run more than one pipeline, tag each one. Same key, different
|
|
228
|
+
label — they appear as separate groups on the dashboard but roll up to
|
|
229
|
+
one organization.
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
kd = Kredisco(
|
|
233
|
+
api_key=os.environ["KREDISCO_API_KEY"],
|
|
234
|
+
workflow_id="invoice-ingest",
|
|
235
|
+
)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Reading scores
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
kd.score(extractor.pubkey) # one number, 300–850
|
|
242
|
+
kd.breakdown(extractor.pubkey) # what the score is made of
|
|
243
|
+
kd.dashboard() # your agents, grouped by pipeline
|
|
244
|
+
kd.best("extract", minimum=650) # highest scorer for a kind of work
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`best` is the point of the whole thing: pick who does the work by track
|
|
248
|
+
record instead of hardcoding a vendor and hoping.
|
|
249
|
+
|
|
250
|
+
## Things worth knowing
|
|
251
|
+
|
|
252
|
+
**Reporting never breaks your pipeline.** If Kredisco is unreachable or
|
|
253
|
+
your key is rejected, the failure is logged and your work continues.
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
import logging
|
|
257
|
+
logging.getLogger("kredisco").setLevel(logging.INFO)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Agent keys live on disk.** Kredisco writes each agent's keypair to
|
|
261
|
+
`.kredisco/`. That directory *is* your agents' identity — lose it and
|
|
262
|
+
every score resets to 300. Back it up, and add it to `.gitignore`.
|
|
263
|
+
|
|
264
|
+
**Timestamps come from the SDK**, not from your agent, so an agent
|
|
265
|
+
cannot shorten its own recorded duration.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## A working example
|
|
272
|
+
|
|
273
|
+
[`examples/langgraph_support_triage.py`](examples/langgraph_support_triage.py)
|
|
274
|
+
is a four-agent LangGraph pipeline built on two different Claude models,
|
|
275
|
+
with a validator on every step.
|
|
276
|
+
|
|
277
|
+
Run it a few times. The scores separate for real reasons — one agent
|
|
278
|
+
returns JSON wrapped in code fences, another hits a response shape the
|
|
279
|
+
caller did not expect, a third is simply slower. Fix the prompt that is
|
|
280
|
+
failing and watch that agent climb over subsequent runs, without ever
|
|
281
|
+
catching the agent that never failed.
|
|
282
|
+
|
|
283
|
+
That gap is the point. History has weight, and recovery takes work.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## What this does not solve
|
|
288
|
+
|
|
289
|
+
**A dishonest reporter can fabricate everything.** Signatures prove who
|
|
290
|
+
signed, not that the content is true. Credit bureaus have the same hole
|
|
291
|
+
and handle it with licensing and liability rather than cryptography.
|
|
292
|
+
Kredisco's mitigation is that keys are tied to GitHub accounts and the
|
|
293
|
+
diversity factor requires many distinct organizations.
|
|
294
|
+
|
|
295
|
+
**Identity is still cheap.** A bad score can be abandoned for a fresh
|
|
296
|
+
keypair. Starting at the floor makes that costly, not impossible.
|
|
297
|
+
|
|
298
|
+
**One score across all task types.** An agent good at summarising and
|
|
299
|
+
bad at code review averages into noise.
|
|
300
|
+
|
|
301
|
+
**Rate limiting is per-process** and does not survive horizontal
|
|
302
|
+
scaling.
|
|
303
|
+
|
|
304
|
+
**No test suite.** The scoring model has changed several times and was
|
|
305
|
+
verified by inspection.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
[](https://www.linkedin.com/in/reachrshah)
|
|
310
|
+
[](https://github.com/idfwyy)
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from requests.adapters import HTTPAdapter
|
|
9
|
+
from urllib3.util.retry import Retry
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from kredisco.identity import Keypair
|
|
13
|
+
from kredisco.receipt import Receipt
|
|
14
|
+
except ModuleNotFoundError:
|
|
15
|
+
from identity import Keypair
|
|
16
|
+
from receipt import Receipt
|
|
17
|
+
|
|
18
|
+
DEFAULT_SERVER = "https://api.kredisco.com"
|
|
19
|
+
DEFAULT_TIMEOUT = 10.0
|
|
20
|
+
DEFAULT_BUDGET = 30.0
|
|
21
|
+
DEFAULT_KEY_DIR = ".kredisco"
|
|
22
|
+
_RAISE = object()
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger("kredisco")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class KrediscoError(Exception):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Agent:
|
|
32
|
+
|
|
33
|
+
def __init__(self, name, specialty, keypair):
|
|
34
|
+
self.name = name
|
|
35
|
+
self.specialty = specialty
|
|
36
|
+
self.keypair = keypair
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def pubkey(self):
|
|
40
|
+
return self.keypair.public_key_hex()
|
|
41
|
+
|
|
42
|
+
def __repr__(self):
|
|
43
|
+
return "<Agent {} {}>".format(self.name, self.pubkey[:8])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Task:
|
|
47
|
+
|
|
48
|
+
def __init__(self):
|
|
49
|
+
self.accepted = True
|
|
50
|
+
self.result = None
|
|
51
|
+
self.error = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Kredisco:
|
|
55
|
+
|
|
56
|
+
def __init__(self, api_key=None, workflow_id=None, server=None,
|
|
57
|
+
key_dir=None, timeout=DEFAULT_TIMEOUT, budget=DEFAULT_BUDGET):
|
|
58
|
+
self.api_key = api_key or os.environ.get("KREDISCO_API_KEY")
|
|
59
|
+
if not self.api_key:
|
|
60
|
+
raise KrediscoError(
|
|
61
|
+
"No API key. Pass api_key= or set KREDISCO_API_KEY. "
|
|
62
|
+
"Create one at your Kredisco dashboard."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
self.workflow_id = workflow_id
|
|
66
|
+
self.server = (server or os.environ.get("KREDISCO_SERVER")
|
|
67
|
+
or DEFAULT_SERVER).rstrip("/")
|
|
68
|
+
self.key_dir = key_dir or os.environ.get("KREDISCO_KEY_DIR", DEFAULT_KEY_DIR)
|
|
69
|
+
self.timeout = timeout
|
|
70
|
+
self.budget = budget
|
|
71
|
+
|
|
72
|
+
self.session = self._build_session()
|
|
73
|
+
self.caller = Keypair.load_or_create(
|
|
74
|
+
os.path.join(self.key_dir, "orchestrator.key")
|
|
75
|
+
)
|
|
76
|
+
self._registered = set()
|
|
77
|
+
|
|
78
|
+
# ---------- plumbing ----------
|
|
79
|
+
|
|
80
|
+
def _build_session(self):
|
|
81
|
+
session = requests.Session()
|
|
82
|
+
session.headers["Authorization"] = "Bearer " + self.api_key
|
|
83
|
+
retry = Retry(
|
|
84
|
+
total=3,
|
|
85
|
+
backoff_factor=0.5,
|
|
86
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
87
|
+
allowed_methods=["GET", "POST"],
|
|
88
|
+
respect_retry_after_header=True,
|
|
89
|
+
)
|
|
90
|
+
adapter = HTTPAdapter(max_retries=retry)
|
|
91
|
+
session.mount("http://", adapter)
|
|
92
|
+
session.mount("https://", adapter)
|
|
93
|
+
return session
|
|
94
|
+
|
|
95
|
+
def _post(self, path, payload):
|
|
96
|
+
try:
|
|
97
|
+
res = self.session.post(self.server + path, json=payload,
|
|
98
|
+
timeout=self.timeout)
|
|
99
|
+
except requests.RequestException as exc:
|
|
100
|
+
logger.warning("kredisco: %s unreachable (%s)", path, exc)
|
|
101
|
+
return None
|
|
102
|
+
if res.status_code == 401:
|
|
103
|
+
logger.error("kredisco: API key rejected")
|
|
104
|
+
elif res.status_code >= 400:
|
|
105
|
+
logger.warning("kredisco: %s returned %s %s",
|
|
106
|
+
path, res.status_code, res.text[:200])
|
|
107
|
+
return res
|
|
108
|
+
|
|
109
|
+
def _get(self, path):
|
|
110
|
+
try:
|
|
111
|
+
res = self.session.get(self.server + path, timeout=self.timeout)
|
|
112
|
+
except requests.RequestException as exc:
|
|
113
|
+
raise KrediscoError("Could not reach Kredisco: {}".format(exc))
|
|
114
|
+
if res.status_code >= 400:
|
|
115
|
+
raise KrediscoError("{} returned {}".format(path, res.status_code))
|
|
116
|
+
return res.json()
|
|
117
|
+
|
|
118
|
+
# ---------- agents ----------
|
|
119
|
+
|
|
120
|
+
def agent(self, name, specialty=None):
|
|
121
|
+
keypair = Keypair.load_or_create(
|
|
122
|
+
os.path.join(self.key_dir, "agents", name + ".key")
|
|
123
|
+
)
|
|
124
|
+
agent = Agent(name, specialty or name, keypair)
|
|
125
|
+
|
|
126
|
+
if agent.pubkey not in self._registered:
|
|
127
|
+
self._post("/register", {
|
|
128
|
+
"pubkey": agent.pubkey,
|
|
129
|
+
"name": agent.name,
|
|
130
|
+
"specialty": agent.specialty,
|
|
131
|
+
})
|
|
132
|
+
self._registered.add(agent.pubkey)
|
|
133
|
+
|
|
134
|
+
return agent
|
|
135
|
+
|
|
136
|
+
# ---------- reporting ----------
|
|
137
|
+
|
|
138
|
+
def _report(self, agent, task_type, started_at, delivered_at,
|
|
139
|
+
accepted, deadline, parent_task_id=None):
|
|
140
|
+
task_id = str(uuid.uuid4())
|
|
141
|
+
receipt = Receipt(
|
|
142
|
+
task_id, task_type, deadline, started_at, delivered_at,
|
|
143
|
+
accepted, parent_task_id=parent_task_id,
|
|
144
|
+
)
|
|
145
|
+
receipt.sign_by(agent.keypair, "specialist")
|
|
146
|
+
receipt.sign_by(self.caller, "hiring")
|
|
147
|
+
|
|
148
|
+
self._post("/settle", {
|
|
149
|
+
"task_id": task_id,
|
|
150
|
+
"task_type": task_type,
|
|
151
|
+
"deadline": deadline,
|
|
152
|
+
"started_at": started_at,
|
|
153
|
+
"delivered_at": delivered_at,
|
|
154
|
+
"accepted": accepted,
|
|
155
|
+
"specialist_sig": receipt.specialist_sig,
|
|
156
|
+
"hiring_sig": receipt.hiring_sig,
|
|
157
|
+
"specialist_pubkey": agent.pubkey,
|
|
158
|
+
"hiring_pubkey": self.caller.public_key_hex(),
|
|
159
|
+
"agent_id": agent.pubkey,
|
|
160
|
+
"caller_id": self.caller.public_key_hex(),
|
|
161
|
+
"parent_task_id": parent_task_id,
|
|
162
|
+
"workflow_id": self.workflow_id,
|
|
163
|
+
})
|
|
164
|
+
return task_id
|
|
165
|
+
|
|
166
|
+
def track(self, agent, task_type, fn, *args,
|
|
167
|
+
validate=None, budget=None, retries=0, default=_RAISE, **kwargs):
|
|
168
|
+
parent_task_id = None
|
|
169
|
+
attempts = retries + 1
|
|
170
|
+
last_error = None
|
|
171
|
+
|
|
172
|
+
for attempt in range(attempts):
|
|
173
|
+
started_at = time.time()
|
|
174
|
+
deadline = started_at + (budget or self.budget)
|
|
175
|
+
accepted = True
|
|
176
|
+
result = None
|
|
177
|
+
last_error = None
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
result = fn(*args, **kwargs)
|
|
181
|
+
if validate is not None:
|
|
182
|
+
accepted = bool(validate(result))
|
|
183
|
+
except Exception as exc:
|
|
184
|
+
accepted = False
|
|
185
|
+
last_error = exc
|
|
186
|
+
|
|
187
|
+
delivered_at = time.time()
|
|
188
|
+
task_id = self._report(agent, task_type, started_at, delivered_at,
|
|
189
|
+
accepted, deadline, parent_task_id)
|
|
190
|
+
|
|
191
|
+
if accepted:
|
|
192
|
+
return result
|
|
193
|
+
|
|
194
|
+
parent_task_id = task_id
|
|
195
|
+
if attempt < attempts - 1:
|
|
196
|
+
logger.info("kredisco: retrying %s on %s", task_type, agent.name)
|
|
197
|
+
|
|
198
|
+
if default is not _RAISE:
|
|
199
|
+
logger.info("kredisco: %s on %s failed, returning default",
|
|
200
|
+
task_type, agent.name)
|
|
201
|
+
return default
|
|
202
|
+
|
|
203
|
+
if last_error is not None:
|
|
204
|
+
raise last_error
|
|
205
|
+
return result
|
|
206
|
+
@contextmanager
|
|
207
|
+
def task(self, agent, task_type, budget=None, parent_task_id=None):
|
|
208
|
+
started_at = time.time()
|
|
209
|
+
deadline = started_at + (budget or self.budget)
|
|
210
|
+
handle = Task()
|
|
211
|
+
try:
|
|
212
|
+
yield handle
|
|
213
|
+
except Exception:
|
|
214
|
+
handle.accepted = False
|
|
215
|
+
self._report(agent, task_type, started_at, time.time(),
|
|
216
|
+
False, deadline, parent_task_id)
|
|
217
|
+
raise
|
|
218
|
+
self._report(agent, task_type, started_at, time.time(),
|
|
219
|
+
handle.accepted, deadline, parent_task_id)
|
|
220
|
+
|
|
221
|
+
# ---------- reading ----------
|
|
222
|
+
|
|
223
|
+
def score(self, pubkey):
|
|
224
|
+
return self._get("/score/" + pubkey)["score"]
|
|
225
|
+
|
|
226
|
+
def breakdown(self, pubkey):
|
|
227
|
+
return self._get("/agent/" + pubkey + "/breakdown")
|
|
228
|
+
|
|
229
|
+
def dashboard(self):
|
|
230
|
+
return self._get("/dashboard")["workflows"]
|
|
231
|
+
|
|
232
|
+
def leaderboard(self):
|
|
233
|
+
return self._get("/leaderboard")["leaderboard"]
|
|
234
|
+
|
|
235
|
+
def best(self, specialty, minimum=0):
|
|
236
|
+
rows = [r for r in self.leaderboard()
|
|
237
|
+
if r.get("specialty") == specialty and r["score"] >= minimum]
|
|
238
|
+
return rows[0] if rows else None
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from cryptography.exceptions import InvalidSignature
|
|
5
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Keypair:
|
|
9
|
+
|
|
10
|
+
def __init__(self, private_key=None):
|
|
11
|
+
self.private_key = private_key or ed25519.Ed25519PrivateKey.generate()
|
|
12
|
+
self.public_key = self.private_key.public_key()
|
|
13
|
+
|
|
14
|
+
def public_key_hex(self) -> str:
|
|
15
|
+
return self.public_key.public_bytes_raw().hex()
|
|
16
|
+
|
|
17
|
+
def private_key_hex(self) -> str:
|
|
18
|
+
return self.private_key.private_bytes_raw().hex()
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_hex(cls, private_key_hex: str) -> "Keypair":
|
|
22
|
+
raw = bytes.fromhex(private_key_hex)
|
|
23
|
+
if len(raw) != 32:
|
|
24
|
+
raise ValueError("private key must be 32 bytes")
|
|
25
|
+
return cls(ed25519.Ed25519PrivateKey.from_private_bytes(raw))
|
|
26
|
+
|
|
27
|
+
def save(self, path) -> None:
|
|
28
|
+
path = Path(path)
|
|
29
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
path.write_text(self.private_key_hex())
|
|
31
|
+
try:
|
|
32
|
+
os.chmod(path, 0o600)
|
|
33
|
+
except OSError:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def load(cls, path) -> "Keypair":
|
|
38
|
+
return cls.from_hex(Path(path).read_text().strip())
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def load_or_create(cls, path) -> "Keypair":
|
|
42
|
+
path = Path(path)
|
|
43
|
+
if path.exists():
|
|
44
|
+
try:
|
|
45
|
+
return cls.load(path)
|
|
46
|
+
except (ValueError, OSError):
|
|
47
|
+
pass
|
|
48
|
+
keypair = cls()
|
|
49
|
+
keypair.save(path)
|
|
50
|
+
return keypair
|
|
51
|
+
|
|
52
|
+
def sign(self, message: str) -> str:
|
|
53
|
+
return self.private_key.sign(message.encode()).hex()
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def verify(public_key_hex: str, message: str, signature_hex: str) -> bool:
|
|
57
|
+
if not public_key_hex or not signature_hex or message is None:
|
|
58
|
+
return False
|
|
59
|
+
try:
|
|
60
|
+
raw = bytes.fromhex(public_key_hex)
|
|
61
|
+
key_obj = ed25519.Ed25519PublicKey.from_public_bytes(raw)
|
|
62
|
+
key_obj.verify(bytes.fromhex(signature_hex), message.encode())
|
|
63
|
+
return True
|
|
64
|
+
except (InvalidSignature, ValueError, TypeError):
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
kp = Keypair()
|
|
70
|
+
print("agent ID:", kp.public_key_hex())
|
|
71
|
+
sig = kp.sign("hello")
|
|
72
|
+
print("real:", Keypair.verify(kp.public_key_hex(), "hello", sig))
|
|
73
|
+
print("fake:", Keypair.verify(kp.public_key_hex(), "hacked", sig))
|
|
74
|
+
|
|
75
|
+
restored = Keypair.from_hex(kp.private_key_hex())
|
|
76
|
+
print("restored matches:", restored.public_key_hex() == kp.public_key_hex())
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import json
|
|
2
|
+
try:
|
|
3
|
+
from kredisco.identity import Keypair
|
|
4
|
+
except ModuleNotFoundError:
|
|
5
|
+
from identity import Keypair
|
|
6
|
+
|
|
7
|
+
class Receipt:
|
|
8
|
+
def __init__(self, task_id, task_type, deadline, started_at, delivered_at, accepted, specialist_sig = None , hiring_sig = None, parent_task_id = None):
|
|
9
|
+
self.task_id = task_id
|
|
10
|
+
self.task_type = task_type
|
|
11
|
+
self.deadline = deadline
|
|
12
|
+
self.started_at = started_at
|
|
13
|
+
self.delivered_at = delivered_at
|
|
14
|
+
self.accepted = accepted
|
|
15
|
+
self.specialist_sig = specialist_sig
|
|
16
|
+
self.hiring_sig = hiring_sig
|
|
17
|
+
self.parent_task_id = parent_task_id
|
|
18
|
+
|
|
19
|
+
def to_text(self) -> str:
|
|
20
|
+
data = {
|
|
21
|
+
"task_id" : self.task_id,
|
|
22
|
+
"task_type" : self.task_type,
|
|
23
|
+
"deadline" : self.deadline,
|
|
24
|
+
"started_at": self.started_at,
|
|
25
|
+
"delivered_at" : self.delivered_at,
|
|
26
|
+
"accepted" : self.accepted
|
|
27
|
+
}
|
|
28
|
+
return json.dumps(data, sort_keys=True)
|
|
29
|
+
|
|
30
|
+
def sign_by(self, keypair, role) -> str:
|
|
31
|
+
sig = keypair.sign(self.to_text())
|
|
32
|
+
if role == "specialist":
|
|
33
|
+
self.specialist_sig = sig
|
|
34
|
+
else:
|
|
35
|
+
self.hiring_sig = sig
|
|
36
|
+
return sig
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def verify_signatures(self,specialist_pubkey,hiring_pubkey) -> bool:
|
|
40
|
+
return Keypair.verify( specialist_pubkey , self.to_text() , self.specialist_sig ) and Keypair.verify( hiring_pubkey , self.to_text() , self.hiring_sig )
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
specialist = Keypair()
|
|
47
|
+
hiring = Keypair()
|
|
48
|
+
|
|
49
|
+
r = Receipt("47", "summarize", 100.0, 90.0, 95.0, True)
|
|
50
|
+
r.sign_by(specialist, "specialist")
|
|
51
|
+
r.sign_by(hiring, "hiring")
|
|
52
|
+
|
|
53
|
+
print("both valid:", r.verify_signatures(specialist.public_key_hex(), hiring.public_key_hex()))
|
|
54
|
+
|
|
55
|
+
fake = Receipt("99", "summarize", 100.0, 90.0, 95.0, True)
|
|
56
|
+
fake.sign_by(specialist, "specialist") # only specialist signs, no hiring countersign
|
|
57
|
+
print("faker valid:", fake.verify_signatures(specialist.public_key_hex(), hiring.public_key_hex()))
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kredisco
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A trust layer for AI agents
|
|
5
|
+
Author: Rahul Sah
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://kredisco.com
|
|
8
|
+
Project-URL: Source, https://github.com/kredisco/Kredisco
|
|
9
|
+
Keywords: ai,agents,trust,reputation,llm,langgraph
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: requests>=2.28
|
|
13
|
+
Requires-Dist: cryptography>=41.0
|
|
14
|
+
|
|
15
|
+
# Kredisco
|
|
16
|
+
|
|
17
|
+
**A trust layer for AI agents.**
|
|
18
|
+
|
|
19
|
+
Humans carry a credit score. It follows you between lenders, it is held
|
|
20
|
+
by a bureau rather than by you, and it is built from what other parties
|
|
21
|
+
report about your behaviour. A landlord who has never met you can decide
|
|
22
|
+
whether to trust you in seconds.
|
|
23
|
+
|
|
24
|
+
AI agents have no equivalent. You wire one into your pipeline and have
|
|
25
|
+
no idea whether it is dependable until something breaks.
|
|
26
|
+
|
|
27
|
+
Kredisco gives every agent a portable trust score, 300 to 850, earned
|
|
28
|
+
from its real track record and held where the agent cannot reach it.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Why agents need one
|
|
33
|
+
|
|
34
|
+
A pipeline with six agents fails quietly. One step degrades, output
|
|
35
|
+
quality drops, and you find out from a user complaint days later.
|
|
36
|
+
Nothing tells you which agent to stop calling.
|
|
37
|
+
|
|
38
|
+
The instinct is to ask the agents, or to collect ratings. Neither
|
|
39
|
+
survives contact with incentives. An agent will always report that it
|
|
40
|
+
did well. Open rating systems get farmed: an audit of ERC-8004's on-chain
|
|
41
|
+
reputation registry found the large majority of reviewers exhibited
|
|
42
|
+
coordinated Sybil behaviour, and after removing them most rated agents
|
|
43
|
+
had no valid feedback left.
|
|
44
|
+
|
|
45
|
+
Credit scoring solved this problem a long time ago, and not with better
|
|
46
|
+
surveys. It solved it structurally: **the party being scored is never
|
|
47
|
+
the party reporting.** Your bank reports your payments. You cannot file
|
|
48
|
+
your own. You cannot edit the file. You cannot see the number until
|
|
49
|
+
someone pulls it.
|
|
50
|
+
|
|
51
|
+
Kredisco applies that structure to agents.
|
|
52
|
+
|
|
53
|
+
## What the score is made of
|
|
54
|
+
|
|
55
|
+
Like FICO, the score is a weighted blend of factors, and like FICO,
|
|
56
|
+
every one of them comes from somewhere the scored party cannot reach.
|
|
57
|
+
|
|
58
|
+
| Factor | Weight | Where it comes from |
|
|
59
|
+
|---|---:|---|
|
|
60
|
+
| Rework rate (inverted) | 0.45 | You retried, reassigned, or the next step errored |
|
|
61
|
+
| On-time rate | 0.20 | Measured by the SDK wrapping the call |
|
|
62
|
+
| Counterparty diversity | 0.20 | How many distinct organizations hired it |
|
|
63
|
+
| History depth | 0.15 | Volume of recorded work |
|
|
64
|
+
|
|
65
|
+
Scores run 300–850. A new agent scores 300, so abandoning a damaged
|
|
66
|
+
identity means abandoning everything it earned.
|
|
67
|
+
|
|
68
|
+
Rework carries the most weight because it is hardest to fake. An agent
|
|
69
|
+
cannot retry itself, cannot stop a downstream step from failing, and
|
|
70
|
+
cannot stop you from routing around it.
|
|
71
|
+
|
|
72
|
+
## Who reports, and why it can't be gamed
|
|
73
|
+
|
|
74
|
+
A lender reports your payment to the bureau. The bureau holds the file.
|
|
75
|
+
A future lender queries the bureau, never you.
|
|
76
|
+
|
|
77
|
+
Kredisco works the same way. Your orchestrator is the reporter, Kredisco
|
|
78
|
+
is the bureau, and the agent is the subject:
|
|
79
|
+
|
|
80
|
+
1. Your agent completes a task
|
|
81
|
+
2. The agent signs a record of it; your orchestrator countersigns
|
|
82
|
+
3. Your orchestrator submits it, authenticated with your API key
|
|
83
|
+
4. Kredisco verifies both signatures and files it under your organization
|
|
84
|
+
5. The score is recomputed from everything on file
|
|
85
|
+
|
|
86
|
+
Both signatures are required, so neither side can act alone. An agent
|
|
87
|
+
cannot manufacture a history, because every entry needs a real
|
|
88
|
+
counterparty to have signed it. And it never holds its own score — when
|
|
89
|
+
someone wants to know whether to trust it, they ask Kredisco.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Quickstart
|
|
94
|
+
|
|
95
|
+
### 1. Get a key
|
|
96
|
+
|
|
97
|
+
Sign in with GitHub at the Kredisco dashboard and create an API key. One
|
|
98
|
+
key covers everything you build.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
export KREDISCO_API_KEY=kd_...
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 2. Install
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pip install kredisco
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 3. Connect
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
import os
|
|
114
|
+
from kredisco import Kredisco
|
|
115
|
+
|
|
116
|
+
kd = Kredisco(api_key=os.environ["KREDISCO_API_KEY"])
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 4. Name each agent you want scored
|
|
120
|
+
|
|
121
|
+
An "agent" is any component you call and could imagine replacing: a
|
|
122
|
+
model behind a prompt, a third-party API, a tool, a subprocess. Give it
|
|
123
|
+
a name once. Kredisco creates its identity and reuses it forever after.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
extractor = kd.agent("invoice-extractor")
|
|
127
|
+
reviewer = kd.agent("code-reviewer")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 5. Route your calls through `track`
|
|
131
|
+
|
|
132
|
+
Take the call you already make:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
data = extract_invoice(pdf)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Hand the function to `track` instead of calling it yourself:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
data = kd.track(extractor, "extract", extract_invoice, pdf)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Four arguments: the agent, a label for the kind of work, **the function
|
|
145
|
+
without parentheses**, then its arguments.
|
|
146
|
+
|
|
147
|
+
Kredisco calls the function, times it, files a record, and hands back
|
|
148
|
+
exactly what your function returned. If it raises, the failure is
|
|
149
|
+
recorded and the exception propagates normally. Your logic does not
|
|
150
|
+
change.
|
|
151
|
+
|
|
152
|
+
That is the integration. Scores start appearing on the dashboard.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Telling Kredisco what counts as a failure
|
|
157
|
+
|
|
158
|
+
By default a task fails only if your function raises.
|
|
159
|
+
|
|
160
|
+
Most pipelines have a stricter rule — a required field, a schema, a
|
|
161
|
+
minimum length. Pass `validate` and Kredisco uses yours:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
data = kd.track(
|
|
165
|
+
extractor, "extract", extract_invoice, pdf,
|
|
166
|
+
validate=lambda d: d is not None and "total" in d,
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**This matters more than it looks.** With no validator and nothing
|
|
171
|
+
raising, every task passes, and the largest factor in the score is
|
|
172
|
+
measuring nothing. Kredisco is worth most to pipelines that already
|
|
173
|
+
check their own output.
|
|
174
|
+
|
|
175
|
+
## Retries
|
|
176
|
+
|
|
177
|
+
Add `retries=1` and a failed attempt runs again. Kredisco links the
|
|
178
|
+
retry to the original task, and that link is what drives the rework
|
|
179
|
+
factor:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
data = kd.track(
|
|
183
|
+
extractor, "extract", extract_invoice, pdf,
|
|
184
|
+
validate=lambda d: d is not None and "total" in d,
|
|
185
|
+
retries=1,
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
If your orchestrator already retries on its own, leave this off and use
|
|
190
|
+
the context manager below for each attempt.
|
|
191
|
+
|
|
192
|
+
## When everything fails
|
|
193
|
+
|
|
194
|
+
If every attempt fails, `track` re-raises the last exception. Sometimes
|
|
195
|
+
that is what you want. Often it is not — a graph node that stops the
|
|
196
|
+
whole run because one step returned bad JSON is worse than a node that
|
|
197
|
+
carries on with an empty value.
|
|
198
|
+
|
|
199
|
+
Pass `default` and `track` returns it instead of raising:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
data = kd.track(
|
|
203
|
+
extractor, "extract", extract_invoice, pdf,
|
|
204
|
+
validate=lambda d: d is not None and "total" in d,
|
|
205
|
+
retries=1,
|
|
206
|
+
default={},
|
|
207
|
+
)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The failure is still recorded and the score still drops. Only your
|
|
211
|
+
control flow changes.
|
|
212
|
+
|
|
213
|
+
Without `default`, every call site needs its own `try`/`except`. With
|
|
214
|
+
it, a pipeline step is one expression:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
def extract_node(state):
|
|
218
|
+
return {"fields": kd.track(
|
|
219
|
+
extractor, "extract", extract_invoice, state["pdf"],
|
|
220
|
+
validate=fields_ok, retries=1, default={},
|
|
221
|
+
)}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## When you cannot hand over a function
|
|
225
|
+
|
|
226
|
+
Some code will not collapse into a single call — the body of a graph
|
|
227
|
+
node, a streamed response consumed in pieces, a block with setup and
|
|
228
|
+
teardown around it. Open a task and report the outcome yourself:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
with kd.task(reviewer, "review") as t:
|
|
232
|
+
findings = run_review(diff)
|
|
233
|
+
t.accepted = len(findings) > 0
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The clock starts when the block opens and stops when it closes. An
|
|
237
|
+
exception inside records a failure and re-raises.
|
|
238
|
+
|
|
239
|
+
## Grouping by pipeline
|
|
240
|
+
|
|
241
|
+
If you run more than one pipeline, tag each one. Same key, different
|
|
242
|
+
label — they appear as separate groups on the dashboard but roll up to
|
|
243
|
+
one organization.
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
kd = Kredisco(
|
|
247
|
+
api_key=os.environ["KREDISCO_API_KEY"],
|
|
248
|
+
workflow_id="invoice-ingest",
|
|
249
|
+
)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Reading scores
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
kd.score(extractor.pubkey) # one number, 300–850
|
|
256
|
+
kd.breakdown(extractor.pubkey) # what the score is made of
|
|
257
|
+
kd.dashboard() # your agents, grouped by pipeline
|
|
258
|
+
kd.best("extract", minimum=650) # highest scorer for a kind of work
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`best` is the point of the whole thing: pick who does the work by track
|
|
262
|
+
record instead of hardcoding a vendor and hoping.
|
|
263
|
+
|
|
264
|
+
## Things worth knowing
|
|
265
|
+
|
|
266
|
+
**Reporting never breaks your pipeline.** If Kredisco is unreachable or
|
|
267
|
+
your key is rejected, the failure is logged and your work continues.
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
import logging
|
|
271
|
+
logging.getLogger("kredisco").setLevel(logging.INFO)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Agent keys live on disk.** Kredisco writes each agent's keypair to
|
|
275
|
+
`.kredisco/`. That directory *is* your agents' identity — lose it and
|
|
276
|
+
every score resets to 300. Back it up, and add it to `.gitignore`.
|
|
277
|
+
|
|
278
|
+
**Timestamps come from the SDK**, not from your agent, so an agent
|
|
279
|
+
cannot shorten its own recorded duration.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## A working example
|
|
286
|
+
|
|
287
|
+
[`examples/langgraph_support_triage.py`](examples/langgraph_support_triage.py)
|
|
288
|
+
is a four-agent LangGraph pipeline built on two different Claude models,
|
|
289
|
+
with a validator on every step.
|
|
290
|
+
|
|
291
|
+
Run it a few times. The scores separate for real reasons — one agent
|
|
292
|
+
returns JSON wrapped in code fences, another hits a response shape the
|
|
293
|
+
caller did not expect, a third is simply slower. Fix the prompt that is
|
|
294
|
+
failing and watch that agent climb over subsequent runs, without ever
|
|
295
|
+
catching the agent that never failed.
|
|
296
|
+
|
|
297
|
+
That gap is the point. History has weight, and recovery takes work.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## What this does not solve
|
|
302
|
+
|
|
303
|
+
**A dishonest reporter can fabricate everything.** Signatures prove who
|
|
304
|
+
signed, not that the content is true. Credit bureaus have the same hole
|
|
305
|
+
and handle it with licensing and liability rather than cryptography.
|
|
306
|
+
Kredisco's mitigation is that keys are tied to GitHub accounts and the
|
|
307
|
+
diversity factor requires many distinct organizations.
|
|
308
|
+
|
|
309
|
+
**Identity is still cheap.** A bad score can be abandoned for a fresh
|
|
310
|
+
keypair. Starting at the floor makes that costly, not impossible.
|
|
311
|
+
|
|
312
|
+
**One score across all task types.** An agent good at summarising and
|
|
313
|
+
bad at code review averages into noise.
|
|
314
|
+
|
|
315
|
+
**Rate limiting is per-process** and does not survive horizontal
|
|
316
|
+
scaling.
|
|
317
|
+
|
|
318
|
+
**No test suite.** The scoring model has changed several times and was
|
|
319
|
+
verified by inspection.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
[](https://www.linkedin.com/in/reachrshah)
|
|
324
|
+
[](https://github.com/idfwyy)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
kredisco/__init__.py
|
|
4
|
+
kredisco/client.py
|
|
5
|
+
kredisco/identity.py
|
|
6
|
+
kredisco/receipt.py
|
|
7
|
+
kredisco.egg-info/PKG-INFO
|
|
8
|
+
kredisco.egg-info/SOURCES.txt
|
|
9
|
+
kredisco.egg-info/dependency_links.txt
|
|
10
|
+
kredisco.egg-info/requires.txt
|
|
11
|
+
kredisco.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
kredisco
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kredisco"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A trust layer for AI agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{name = "Rahul Sah"}]
|
|
13
|
+
keywords = ["ai", "agents", "trust", "reputation", "llm", "langgraph"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"requests>=2.28",
|
|
16
|
+
"cryptography>=41.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://kredisco.com"
|
|
21
|
+
Source = "https://github.com/kredisco/Kredisco"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools]
|
|
24
|
+
packages = ["kredisco"]
|
kredisco-0.1.0/setup.cfg
ADDED