kira-wallet 0.1.6 → 0.1.7
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.
- package/README.md +3 -4
- package/THIRD_PARTY_NOTICES.md +6 -0
- package/docs/AGENT_TOOLS.md +13 -0
- package/docs/CLI.md +2 -2
- package/docs/FREE_RPC.md +36 -9
- package/docs/JOB_CONTRACT.md +24 -0
- package/docs/RELEASE.md +25 -6
- package/kira_agent.py +21 -8
- package/kira_cli.py +1 -1
- package/kira_config.py +3 -2
- package/kira_jobs.py +81 -19
- package/kira_research_tools.py +1 -1
- package/package.json +5 -3
- package/pipeline-onchain.cjs +54 -36
- package/public_discovery.py +111 -0
- package/sources/TOKEN_LIST_LICENSE.txt +674 -0
- package/sources/job-envelope-v1.schema.json +14 -0
- package/sources/known-tokens.json +357 -22
- package/viewer/model.py +3 -0
- package/viewer/onboarding.py +2 -1
- package/viewer/static/app.js +5 -2
- package/viewer/static/chat-workspace.js +2 -2
- package/viewer/static/jobs.js +33 -12
- package/viewer/static/report.js +1 -1
- package/viewer/static/watching.js +3 -3
- package/viewer/static/workspace-model.js +6 -5
- package/wallet.py +130 -21
package/README.md
CHANGED
|
@@ -43,12 +43,11 @@ For a viewer without wallet or research actions, use `kira start --read-only`.
|
|
|
43
43
|
Prefer an archive? [GitHub releases](https://github.com/realproject7/kira-wallet/releases/tag/v0.1.2)
|
|
44
44
|
include the same package and its checksum.
|
|
45
45
|
|
|
46
|
-
1. **
|
|
47
|
-
ERC20 discovery.
|
|
46
|
+
1. **Review optional coverage.** Free research is enabled by default. In **Workspace settings**, connect Alchemy for broader
|
|
47
|
+
ERC20 discovery. Free token sources have limited coverage; incomplete coverage
|
|
48
48
|
stays visible. An AI or browser wallet connection does not enable an indexer.
|
|
49
49
|
2. **Add a watching wallet.** Paste a public address and give it a name. Add more
|
|
50
|
-
wallets whenever you need them.
|
|
51
|
-
offers limited research of native and Mint Club assets.
|
|
50
|
+
wallets whenever you need them. The form starts free research of native, common tokens and Mint Club assets, with keyless discovery on supported networks. First balances appear before detailed research finishes.
|
|
52
51
|
3. **Connect your AI.** Install and sign in to the
|
|
53
52
|
[Codex CLI](https://developers.openai.com/codex/cli) or
|
|
54
53
|
[Claude Code](https://code.claude.com/docs/en/quickstart), then choose your
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -28,3 +28,9 @@ identifies the OWS wallet-creation integration without implying endorsement.
|
|
|
28
28
|
Its retrieval date and source digest are in `site/assets/ows-artwork-source.json`.
|
|
29
29
|
|
|
30
30
|
CASHCAT, BLAST and USDB artwork in the public reconstructed demo comes from lpTOKEN.fun token metadata, DEX Screener and CoinGecko. Token marks belong to their respective owners and identify the assets without implying affiliation or endorsement. The source URLs and chain-specific contract identities are recorded in `site/assets/token-artwork-sources.json`.
|
|
31
|
+
|
|
32
|
+
The factual token inventory in `sources/known-tokens.json` includes selected
|
|
33
|
+
chain/contract identities from Uniswap default-token-list at commit
|
|
34
|
+
583cb696cc15778536fe0ddd416de5bd81be2023. The upstream references retain
|
|
35
|
+
GPL-3.0 terms in `sources/TOKEN_LIST_LICENSE.txt`. Per-contract provenance is
|
|
36
|
+
recorded in the catalog. No upstream executable code or artwork is included.
|
package/docs/AGENT_TOOLS.md
CHANGED
|
@@ -60,3 +60,16 @@ through the user's CLI; local storage does not imply an offline model.
|
|
|
60
60
|
|
|
61
61
|
See [the conversation contract](ONBOARDING_CHAT_CONTRACT.md).
|
|
62
62
|
The standalone stdio MCP adapter above remains read-only and separate.
|
|
63
|
+
|
|
64
|
+
Native responses select the final Codex agent message. Older verified event
|
|
65
|
+
streams without a phase use their last agent message. Commentary cannot become
|
|
66
|
+
a tool request by concatenation. A mixed or malformed request gets one bounded
|
|
67
|
+
correction; no tool executes from it and repeated malformed protocol fails
|
|
68
|
+
without adding raw JSON to conversation history. Turn receipts expose only
|
|
69
|
+
bounded tool names and completed/failed outcomes.
|
|
70
|
+
|
|
71
|
+
Job reads include analysis phase, parent/continuation IDs and real progress
|
|
72
|
+
events. Normalized chain discovery completeness is separate from a job's
|
|
73
|
+
on-chain checkpoint status. Deferred candidates and pending networks are not
|
|
74
|
+
failed reads or zero holdings. Historical observations cannot establish current
|
|
75
|
+
execution, transfers or realized profit.
|
package/docs/CLI.md
CHANGED
|
@@ -10,7 +10,7 @@ npm install -g kira-wallet
|
|
|
10
10
|
kira setup
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
The operator published npm version 0.1.
|
|
13
|
+
The operator published npm version 0.1.6. Version 0.1.7 is a preparation candidate. GitHub v0.1.3 remains
|
|
14
14
|
a draft; the previous archive and checksum are available from the
|
|
15
15
|
[0.1.2 GitHub release](https://github.com/realproject7/kira-wallet/releases/tag/v0.1.2).
|
|
16
16
|
Registry publication remains an operator action. The code and original character assets use the MIT license.
|
|
@@ -46,7 +46,7 @@ Published 0.1.4 explains limited discovery before wallet registration and beside
|
|
|
46
46
|
holdings. Missing coverage keeps the portfolio estimate partial. Existing snapshots require a holdings refresh after connecting
|
|
47
47
|
an indexer; a price refresh does not discover missing tokens.
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Free research checks known assets, Mint Club and keyless token candidates on supported networks. Alchemy is optional. For broader ERC20 discovery,
|
|
50
50
|
open Custom RPC & token coverage, select Alchemy and follow its local key guide.
|
|
51
51
|
Published 0.1.5 uses one save for Alchemy RPC and discovery; chain overrides
|
|
52
52
|
remain under Advanced. Never paste the key into the browser. `kira doctor`
|
package/docs/FREE_RPC.md
CHANGED
|
@@ -52,15 +52,42 @@ Registry identity caching remains separate from balances.
|
|
|
52
52
|
|
|
53
53
|
Native balances and the selected issuer/deployment-backed contracts in
|
|
54
54
|
`sources/known-tokens.json` are checked before cold Mint Club enumeration.
|
|
55
|
-
The
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
The catalog has 57 chain/contract identities on eight mainnets. Issuer references
|
|
56
|
+
and selected Uniswap token-list records retain per-contract provenance. The
|
|
57
|
+
upstream list commit and its license are bundled; no untrusted runtime list is
|
|
58
|
+
loaded. Decimals and quantities come from on-chain reads. This remains a bounded
|
|
59
|
+
inventory, not a claim to discover every wrapped or arbitrary token.
|
|
60
|
+
|
|
61
|
+
Default research also uses Routescan without a key on individually verified
|
|
62
|
+
Ethereum, Avalanche and Blast routes. Its documented free access is limited to
|
|
63
|
+
two requests per second and 10,000 calls per day. Kira starts at most one call
|
|
64
|
+
per second, reads at most 20 pages of 100 candidates per chain and spends at most
|
|
65
|
+
25 seconds per enumeration. An isolated HTTP reader enforces a total deadline,
|
|
66
|
+
including DNS and response bodies. Unsafe pagination, outages, malformed data
|
|
67
|
+
and limits keep coverage partial. Provider quantities and prices never replace
|
|
68
|
+
RPC-verified balances. Other Routescan chains are not presumed supported.
|
|
69
|
+
Explicit keyless opt-outs and custom-only preferences remain respected.
|
|
70
|
+
|
|
71
|
+
Initial reports check Base, Ethereum and Blast, up to 128 candidates per chain.
|
|
72
|
+
Selected common contracts take priority. Remaining candidates and networks are
|
|
73
|
+
explicitly pending. A durable background continuation scans every configured
|
|
74
|
+
network, the full Mint Club registry and markets. Registry identity prefixes
|
|
75
|
+
are saved incrementally; balances are never shared between wallets. Batches
|
|
76
|
+
contain at most 128 reads, with splitting only for verified payload limits.
|
|
77
|
+
New initial work takes priority over detailed research. Older same-wallet
|
|
78
|
+
continuations cannot replace newer evidence.
|
|
79
|
+
|
|
80
|
+
Public RPC cannot enumerate all arbitrary ERC20 contracts. Alchemy is an
|
|
81
|
+
optional broader candidate source and backup RPC. Discovery calls made through
|
|
82
|
+
the wallet engine have a 25-second initial or 60-second detailed per-chain
|
|
83
|
+
budget. Native balances, free candidates and Mint Club research remain usable
|
|
84
|
+
without it. Missing or deferred evidence stays unknown. Persistent coverage
|
|
85
|
+
notices distinguish pending work from failed reads and offer retry or optional
|
|
86
|
+
[Alchemy setup](ALCHEMY_SETUP.md).
|
|
87
|
+
|
|
88
|
+
Keyless sources: [Routescan addresses](https://routescan.io/docs/api/addresses),
|
|
89
|
+
[API access](https://routescan.io/docs/plans-and-limits/api-keys-and-pricing),
|
|
90
|
+
[rate limits](https://routescan.io/docs/plans-and-limits/rate-limits).
|
|
64
91
|
|
|
65
92
|
## Read preferences
|
|
66
93
|
|
package/docs/JOB_CONTRACT.md
CHANGED
|
@@ -121,3 +121,27 @@ its projection receipt without another provider request.
|
|
|
121
121
|
|
|
122
122
|
The read-only stdio MCP adapter uses the same job and snapshot service. It has
|
|
123
123
|
no mutation tools and requires separate operator-approved client setup.
|
|
124
|
+
|
|
125
|
+
## Initial evidence and continuations in 0.1.7
|
|
126
|
+
|
|
127
|
+
A valid wallet admission records its identity under a short registry lock.
|
|
128
|
+
Idempotent replay repairs missing admission without undoing later name edits.
|
|
129
|
+
New analysis jobs have `analysis_phase: baseline`. The first usable chain
|
|
130
|
+
publishes an immutable `initial/` snapshot; the running job can reference it
|
|
131
|
+
without claiming that the job is finished. The baseline publishes its own final
|
|
132
|
+
partial snapshot and admits exactly one durable `enrichment` continuation with
|
|
133
|
+
`parent_job_id`. The parent records `continuation_id`.
|
|
134
|
+
|
|
135
|
+
Baseline work runs before enrichment. New baseline admission can preempt a
|
|
136
|
+
running continuation. Saved chain evidence and verified registry identity
|
|
137
|
+
prefixes survive; an interrupted registry batch is read again. A newer request
|
|
138
|
+
for the same wallet supersedes older detailed work. Generation checks prevent
|
|
139
|
+
an older result from replacing the latest pointer, while preserving historical
|
|
140
|
+
evidence. Explicit cancellation of baseline prevents automatic continuation.
|
|
141
|
+
|
|
142
|
+
`onchain_progress` events identify the actual operation, chain, checked count
|
|
143
|
+
and known total. `chain.status: complete` concerns the on-chain checkpoint.
|
|
144
|
+
Normalized wallet `complete` concerns general token discovery. Registry coverage,
|
|
145
|
+
candidate failures, deferred candidates and pending RPC reads are separate facts.
|
|
146
|
+
These flags do not prove exhaustive holdings. First results can be read while
|
|
147
|
+
background work remains active.
|
package/docs/RELEASE.md
CHANGED
|
@@ -49,15 +49,14 @@ Claude model choices, exposes model and permission changes in Workspace settings
|
|
|
49
49
|
and provides one Alchemy connection save. Its registry version was confirmed on
|
|
50
50
|
2026-10-09. Published package bytes remain immutable.
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Published **0.1.6** adds a paced local RPC pool, core-chain public
|
|
53
53
|
alternatives, public-first custom backup, fixed-block hash checks and a small
|
|
54
54
|
known-token catalog. Home and wallet pages display incomplete coverage with an
|
|
55
55
|
Alchemy setup action. The settings guide explains account setup, private local
|
|
56
56
|
key import and an explicit holdings refresh. Missing or malformed discovery data
|
|
57
57
|
cannot become a completed empty inventory.
|
|
58
58
|
|
|
59
|
-
Version 0.1.6
|
|
60
|
-
published. The showcase remains a separate deployment. See
|
|
59
|
+
Version 0.1.6 is published and immutable. The showcase remains a separate deployment. See
|
|
61
60
|
[free RPC behavior](FREE_RPC.md) and [Alchemy setup](ALCHEMY_SETUP.md).
|
|
62
61
|
|
|
63
62
|
The operator published npm **0.1.2** on 2026-10-05. Its registry archive was
|
|
@@ -80,7 +79,7 @@ The earlier unpublished 0.1.2 candidate is preserved in the ignored
|
|
|
80
79
|
`dist/preserved-0.1.2-bad8f02/` directory. Use the current `dist/release.json`
|
|
81
80
|
and its matching archive for publication; do not mix manifests from candidates.
|
|
82
81
|
|
|
83
|
-
Keep GitHub v0.1.1, its tag and its released archive immutable. Versions 0.1.2, 0.1.3, 0.1.4 and 0.1.
|
|
82
|
+
Keep GitHub v0.1.1, its tag and its released archive immutable. Versions 0.1.2, 0.1.3, 0.1.4, 0.1.5 and 0.1.6 are also published and immutable. Do not rebuild them from later documentation
|
|
84
83
|
commits. Future package changes need a new version. Release preparation does not
|
|
85
84
|
publish a GitHub release, merge PR 19, promote the website or publish to npm.
|
|
86
85
|
Registry publication remains operator-only. The coding agent never publishes.
|
|
@@ -95,18 +94,38 @@ python3 scripts/prepare-release.py
|
|
|
95
94
|
```
|
|
96
95
|
|
|
97
96
|
`prepare-release.py` scans the package boundary and creates
|
|
98
|
-
`dist/kira-wallet-0.1.
|
|
97
|
+
`dist/kira-wallet-0.1.7.tgz`. The ignored `dist/release.json` records its source
|
|
99
98
|
commit, SHA-256 and member count. It never publishes. After reviewing that
|
|
100
99
|
manifest, the operator's final command from the project root is:
|
|
101
100
|
|
|
102
101
|
```sh
|
|
103
|
-
npm publish ./dist/kira-wallet-0.1.
|
|
102
|
+
npm publish ./dist/kira-wallet-0.1.7.tgz --access public
|
|
104
103
|
```
|
|
105
104
|
|
|
106
105
|
Use the operator's npm account and complete authentication or OTP personally.
|
|
107
106
|
After publication, verify the registry archive and perform a fresh install
|
|
108
107
|
before marking the new onboarding flow as available to npm users.
|
|
109
108
|
|
|
109
|
+
## Proposed 0.1.7 behavior
|
|
110
|
+
|
|
111
|
+
The candidate enables useful keyless research by default, expands the curated
|
|
112
|
+
contract inventory and separates first recorded balances from exhaustive
|
|
113
|
+
background research. Pending candidates, registry work and other networks stay
|
|
114
|
+
visible. Current wallet evidence cannot roll back when an older continuation
|
|
115
|
+
resumes. See [coverage and limits](FREE_RPC.md).
|
|
116
|
+
|
|
117
|
+
Codex uses the final native message rather than joined commentary. Mixed tool
|
|
118
|
+
protocol receives one bounded correction and is never rendered as a completed
|
|
119
|
+
answer. Claude retains its restricted native adapter. Tool receipts contain
|
|
120
|
+
names and outcomes, without raw arguments. Job checkpoints and normalized token
|
|
121
|
+
discovery flags have separate meanings. Imported connection references reconcile
|
|
122
|
+
with untouched settings fields while preserving edited drafts.
|
|
123
|
+
|
|
124
|
+
The next archive is **0.1.7**, unpublished until the operator runs the command.
|
|
125
|
+
Release readiness requires the repository suite, clean installation, source and
|
|
126
|
+
history privacy scans, independent review and the frozen archive manifest.
|
|
127
|
+
No live-extension or human passphrase acceptance is inferred from these checks.
|
|
128
|
+
|
|
110
129
|
## Verified 0.1.0 publication
|
|
111
130
|
|
|
112
131
|
The operator published 0.1.0, verified on 2026-10-04. The registry archive has
|
package/kira_agent.py
CHANGED
|
@@ -42,7 +42,7 @@ SYSTEM = ('You are Kira, a careful wallet research partner. Answer in the langua
|
|
|
42
42
|
'and full-balance output only when a validated quote exists. DEX pool TVL is not a sell quote. '
|
|
43
43
|
'Missing execution quotes mean sale proceeds, fees, gas and price impact are unknown. '
|
|
44
44
|
'For network questions, group by wallet and distinguish known assets from missing coverage. '
|
|
45
|
-
'
|
|
45
|
+
'In normalized wallet facts, chain complete means general ERC20 discovery was checked. In job chains, status complete means an on-chain checkpoint finished. These are different stages. Neither flag alone proves all holdings are known. registry_complete and candidate_balance_errors describe separate read coverage. An indexer check and unavailable RPC are separate coverage facts, not conflicting records. '
|
|
46
46
|
'An explicit chains.native_balance of zero is an observed native balance at its recorded time and block; null is unknown. Empty positive-holding inventories do not erase these observations. '
|
|
47
47
|
'For saved-analysis comparisons, use snapshots_compare and read both snapshots. coverage_changed compares full evidence, including times and blocks, not just network status flags. price_references_changed may include changed reference values or times, not balances. '
|
|
48
48
|
'Snapshot reads are normalized evidence. Compare chain observations and snapshot_price_references; never claim these are the only raw fields that changed. '
|
|
@@ -140,7 +140,7 @@ def wallet_facts(wallets, *, pool_budget=40_000):
|
|
|
140
140
|
for wallet in wallets:
|
|
141
141
|
row = pick(wallet,('address','tags','name','analysed_at','prices_at','balance_observed_at','known_value_usd','unpriced_count','status'))
|
|
142
142
|
if not wallet.get('analysed_at'): row['known_value_usd']=None
|
|
143
|
-
row['chains'] = [pick(c,('id','name','environment','complete','rpc_available','native_symbol','native_balance','native_observed_at','native_block_number','registry_block_number')) for c in wallet['chains']]
|
|
143
|
+
row['chains'] = [pick(c,('id','name','environment','complete','rpc_available','native_symbol','native_balance','native_observed_at','native_block_number','registry_block_number','discovery_status','registry_complete','registry_phase','rpc_pending','candidate_balance_errors','candidate_deferred')) for c in wallet['chains']]
|
|
144
144
|
row['snapshot_price_references'] = wallet.get('snapshot_price_references',{})
|
|
145
145
|
row['assets'] = []
|
|
146
146
|
for asset in wallet['assets']:
|
|
@@ -211,11 +211,14 @@ def run_native(config, prompt, cancel, *, timeout=180):
|
|
|
211
211
|
else:
|
|
212
212
|
events = [json.loads(line) for line in output.splitlines() if line.strip()]
|
|
213
213
|
if not any(e.get('type') == 'turn.completed' for e in events): raise ValueError()
|
|
214
|
-
|
|
214
|
+
messages = [e['item'] for e in events if e.get('type') == 'item.completed' and e.get('item',{}).get('type') == 'agent_message']
|
|
215
|
+
finals = [m for m in messages if m.get('phase')=='final_answer']
|
|
216
|
+
# Older verified CLIs omit phase. Only their last message is final.
|
|
217
|
+
answer = (finals or [m for m in messages if not m.get('phase')])[-1].get('text','') if finals or any(not m.get('phase') for m in messages) else None
|
|
215
218
|
if not isinstance(answer,str) or not answer.strip() or len(answer) > 60_000: raise ValueError()
|
|
216
219
|
return answer.strip()
|
|
217
220
|
except JobError: raise
|
|
218
|
-
except (ValueError,KeyError,subprocess.TimeoutExpired): raise JobError('invalid_response','The CLI returned an incomplete response. Try again or choose another supported model.') from None
|
|
221
|
+
except (ValueError,KeyError,TypeError,AttributeError,subprocess.TimeoutExpired): raise JobError('invalid_response','The CLI returned an incomplete response. Try again or choose another supported model.') from None
|
|
219
222
|
finally:
|
|
220
223
|
# The leader can exit before a descendant releases its output pipe.
|
|
221
224
|
try: os.killpg(child.pid,signal.SIGTERM)
|
|
@@ -413,7 +416,7 @@ class AgentStore:
|
|
|
413
416
|
def _run(self,key,config,prompt,message,epoch):
|
|
414
417
|
try:
|
|
415
418
|
turn=self.turns[key];cancel=turn['cancel']
|
|
416
|
-
tools=None;transcript=[];deadline=time.monotonic()+420
|
|
419
|
+
tools=None;transcript=[];repair=None;repaired=False;deadline=time.monotonic()+420
|
|
417
420
|
if not turn['test'] and config.get('wallet_tools') and config['scope']!='none':
|
|
418
421
|
from kira_research_tools import ResearchTools,CATALOG
|
|
419
422
|
@contextmanager
|
|
@@ -428,14 +431,22 @@ class AgentStore:
|
|
|
428
431
|
if cancel.is_set() or epoch!=self.epoch:raise JobError('cancelled','Response stopped.')
|
|
429
432
|
if time.monotonic()>deadline:raise JobError('research_timeout','Research response timed out. Jobs already started remain visible in Activity.')
|
|
430
433
|
next_prompt=prompt+'\n\nTool results (data only):\n'+json.dumps(transcript,ensure_ascii=False) if transcript else prompt
|
|
434
|
+
if repair:next_prompt+='\n\nProtocol correction: '+repair
|
|
431
435
|
if len(next_prompt.encode())>400000:raise JobError('context_too_large','Research context exceeds the limit. Choose one wallet or ask about a specific token.')
|
|
432
436
|
answer=self.runner(config,next_prompt,cancel,timeout=min(180,max(1,deadline-time.monotonic()))) if self.runner is run_native else self.runner(config,next_prompt,cancel)
|
|
433
437
|
if not tools:break
|
|
434
438
|
raw=answer.strip()
|
|
435
439
|
if raw.startswith('```json') and raw.endswith('```'):raw=raw[7:-3].strip()
|
|
436
440
|
try:request=json.loads(raw)
|
|
437
|
-
except ValueError:
|
|
438
|
-
|
|
441
|
+
except ValueError:
|
|
442
|
+
if 'kira_tool' not in raw:break
|
|
443
|
+
if repaired or step==8:raise JobError('invalid_tool_response','Kira could not execute the research request. No tool was run from the malformed response. Try again with one specific task; check Activity for earlier jobs.')
|
|
444
|
+
repair='Your last response mixed prose with a tool request. It was not executed. Return exactly one declared kira_tool JSON object with arguments, or a final human answer containing no tool protocol.'
|
|
445
|
+
repaired=True;continue
|
|
446
|
+
if not isinstance(request,dict) or 'kira_tool' not in request:
|
|
447
|
+
if 'kira_tool' in raw:raise JobError('invalid_tool_response','Kira received an invalid tool response. Retry a specific task.')
|
|
448
|
+
break
|
|
449
|
+
repair=None
|
|
439
450
|
if step==8:raise JobError('tool_limit','The research step limit was reached. Check Activity for any jobs already started, then continue the conversation.')
|
|
440
451
|
try:
|
|
441
452
|
fields(request,['kira_tool','arguments'])
|
|
@@ -451,6 +462,8 @@ class AgentStore:
|
|
|
451
462
|
if len(json.dumps(transcript+[item],ensure_ascii=False).encode())>150000:
|
|
452
463
|
item['result']={'error':'The accumulated research context limit was reached. Use a specific token or finish with the evidence already received.'}
|
|
453
464
|
transcript.append(item)
|
|
465
|
+
with self.lock:
|
|
466
|
+
self.turns[key].setdefault('tool_calls',[]).append({'name':request['kira_tool'],'status':'failed' if isinstance(result,dict) and result.get('error') else 'completed'})
|
|
454
467
|
with self.lock:self.tool_status='Kira is analysing the results…'
|
|
455
468
|
|
|
456
469
|
with self.lock:
|
|
@@ -474,7 +487,7 @@ class AgentStore:
|
|
|
474
487
|
def read(self,key):
|
|
475
488
|
with self.lock:
|
|
476
489
|
if key not in self.turns:raise JobError('not_found','This response is no longer available. Start a new conversation.')
|
|
477
|
-
return {**{k:self.turns[key][k] for k in ('id','state','error','answer','test','message','conversation_id')},'tool_status':self.tool_status if self.active==key else None}
|
|
490
|
+
return {**{k:self.turns[key][k] for k in ('id','state','error','answer','test','message','conversation_id')},'tool_calls':self.turns[key].get('tool_calls',[]),'tool_status':self.tool_status if self.active==key else None}
|
|
478
491
|
|
|
479
492
|
def cancel(self,key):
|
|
480
493
|
with self.lock:
|
package/kira_cli.py
CHANGED
|
@@ -235,7 +235,7 @@ def main():
|
|
|
235
235
|
from kira_config import provider_config
|
|
236
236
|
cfg=provider_config(cfg,'alchemy',args.key_env)
|
|
237
237
|
cfg['secret_env_file']=str(file);cfg['discovery']['explorers']=True
|
|
238
|
-
elif args.command=='discovery':cfg['discovery']={'provider':args.provider,'key_env':args.key_env,'explorers':args.explorers}
|
|
238
|
+
elif args.command=='discovery':cfg['discovery']={**cfg['discovery'],'provider':args.provider,'key_env':args.key_env,'explorers':args.explorers}
|
|
239
239
|
elif args.setting=='public':cfg['rpc']['mode']='public'
|
|
240
240
|
else:
|
|
241
241
|
cfg['rpc'].update(mode='custom')
|
package/kira_config.py
CHANGED
|
@@ -17,7 +17,7 @@ def config_path():
|
|
|
17
17
|
|
|
18
18
|
def default_config():
|
|
19
19
|
return {'schema_version':1,'rpc':{'mode':'public','allow_public_fallback':True,'priority':'public_first','chains':{}},
|
|
20
|
-
'discovery':{'provider':'none','key_env':'ALCHEMY_API_KEY','explorers':False}}
|
|
20
|
+
'discovery':{'provider':'none','key_env':'ALCHEMY_API_KEY','explorers':False,'public':True}}
|
|
21
21
|
|
|
22
22
|
def provider_config(config, provider, key_env):
|
|
23
23
|
"""One settings transition. Preserve explicit fallback and custom overrides."""
|
|
@@ -45,7 +45,8 @@ def load_config():
|
|
|
45
45
|
if not isinstance(discovery,dict) or discovery.get('provider') not in ('none','alchemy') or type(discovery.get('explorers',False)) is not bool:
|
|
46
46
|
raise ValueError('Invalid discovery configuration.')
|
|
47
47
|
if rpc.get('priority','public_first') not in ('public_first','custom_first'):raise ValueError('Invalid RPC priority.')
|
|
48
|
-
if
|
|
48
|
+
if type(discovery.get('public',True)) is not bool:raise ValueError('Invalid public discovery preference.')
|
|
49
|
+
if set(rpc)-{'mode','allow_public_fallback','priority','chains'} or set(discovery)-{'provider','key_env','explorers','public'}:raise ValueError('Unknown provider configuration fields.')
|
|
49
50
|
if any(not isinstance(row,dict) or set(row)-{'url_env','alchemy_network'} for row in rpc['chains'].values()):raise ValueError('Unknown RPC endpoint fields.')
|
|
50
51
|
for name in [discovery.get('key_env'),*[row.get('url_env') if isinstance(row,dict) else None for row in rpc['chains'].values()]]:
|
|
51
52
|
if not isinstance(name,str) or not re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*',name):raise ValueError('Credentials must use valid environment references.')
|
package/kira_jobs.py
CHANGED
|
@@ -123,13 +123,16 @@ class JobStore:
|
|
|
123
123
|
job = read(path)
|
|
124
124
|
if not isinstance(job,dict) or type(job.get('schema_version')) is not int or job.get('schema_version') != 1: raise JobError('unsupported_version', 'Unsupported persisted job version.')
|
|
125
125
|
required={'schema_version','job_id','operation','input','state','attempt','sequence','result','previous_result','key_hash','request_hash','created_at','updated_at','cancel_requested','checkpoint','stage','events','errors','chains'}
|
|
126
|
-
optional={'raw_hash','control_receipts','control_raw','provider_configuration','started_at'}
|
|
126
|
+
optional={'raw_hash','control_receipts','control_raw','provider_configuration','started_at','analysis_phase','parent_job_id','continuation_id'}
|
|
127
127
|
if required-set(job) or set(job)-required-optional or job['job_id']!=job_id or job['checkpoint']!='snapshots/jobs/'+job_id or not isinstance(job['operation'],str) or job['operation'] not in {'wallet.add','wallet.refresh','prices.refresh','wallet.setTags','settings.rpc','settings.discovery','settings.provider'} or not isinstance(job['state'],str) or job['state'] not in TERMINAL|{'queued','running'} or any(type(job[k]) is not int or job[k]<0 for k in ('attempt','sequence')) or type(job['cancel_requested']) is not bool or not isinstance(job['input'],dict) or any(not isinstance(job[k],list) for k in ('events','errors')) or not isinstance(job['chains'],dict):
|
|
128
128
|
raise JobError('invalid_envelope','Persisted job validation failed. Preserve the file for recovery.')
|
|
129
129
|
names={'wallet.add':['address','tag'],'wallet.refresh':['wallet'],'prices.refresh':['wallet'],'wallet.setTags':['wallet','tags'],
|
|
130
130
|
'settings.rpc':['mode','chains','allow_public_fallback'],'settings.discovery':['provider','key_env'],'settings.provider':['provider','key_env']}[job['operation']]
|
|
131
131
|
if job['operation']=='settings.rpc' and 'priority' in job['input']:names=names+['priority']
|
|
132
132
|
fields(job['input'],names)
|
|
133
|
+
if 'analysis_phase' in job and job['analysis_phase'] not in ('baseline','enrichment'):raise JobError('invalid_envelope','Invalid analysis phase.')
|
|
134
|
+
for field in ('parent_job_id','continuation_id'):
|
|
135
|
+
if field in job:identifier(job[field])
|
|
133
136
|
wallet=job['input'].get('wallet') or job['input'].get('address')
|
|
134
137
|
if wallet is not None and (not isinstance(wallet,str) or not ADDRESS.fullmatch(wallet)):raise JobError('invalid_envelope','Persisted wallet identity is invalid.')
|
|
135
138
|
return job
|
|
@@ -206,7 +209,27 @@ class JobStore:
|
|
|
206
209
|
return value.copy()
|
|
207
210
|
raise JobError('unsupported_operation', 'Unsupported job operation.')
|
|
208
211
|
|
|
209
|
-
def
|
|
212
|
+
def admit_wallet(self, job):
|
|
213
|
+
if job['operation'] not in ('wallet.add','wallet.refresh'):return job
|
|
214
|
+
from wallet import register, registry_locked
|
|
215
|
+
with registry_locked(self.root):
|
|
216
|
+
registry=read(self.root/'wallets.json')
|
|
217
|
+
admissions=registry.setdefault('wallet_admissions',{})
|
|
218
|
+
address=job['input'].get('address') or job['input'].get('wallet')
|
|
219
|
+
entry=next((w for w in registry['wallets'] if w['address_key']==address.lower()),None)
|
|
220
|
+
if job['job_id'] not in admissions:
|
|
221
|
+
legacy_done=not job.get('analysis_phase') and (job['state']!='queued' or job['attempt']>0) and entry is not None
|
|
222
|
+
if job['operation']=='wallet.add' and not legacy_done:
|
|
223
|
+
entry=register(registry,address,job['input']['tag'])
|
|
224
|
+
if job.get('analysis_phase')=='baseline':entry['research_generation']=job['job_id']
|
|
225
|
+
admissions[job['job_id']]=address.lower()
|
|
226
|
+
atomic(self.root/'wallets.json',registry)
|
|
227
|
+
elif entry is None and job['operation']=='wallet.add':
|
|
228
|
+
register(registry,address,job['input']['tag'])
|
|
229
|
+
atomic(self.root/'wallets.json',registry)
|
|
230
|
+
return job
|
|
231
|
+
|
|
232
|
+
def submit(self, request, *, phase='baseline', parent=None):
|
|
210
233
|
fields(request, ['schema_version', 'operation', 'input', 'idempotency_key'])
|
|
211
234
|
if type(request['schema_version']) is not int or request['schema_version'] != 1:
|
|
212
235
|
raise JobError('unsupported_version', 'Expected request schema version 1.')
|
|
@@ -218,19 +241,19 @@ class JobStore:
|
|
|
218
241
|
if not isinstance(operation,str):raise JobError('unsupported_operation','Expected a typed operation name.')
|
|
219
242
|
key_hash = digest(key); receipt = self.jobs / 'requests' / (key_hash + '.json')
|
|
220
243
|
raw_hash=digest({'operation':operation,'input':request['input']})
|
|
221
|
-
if receipt.exists() and read(receipt).get('raw_hash')==raw_hash:return self.get(read(receipt)['job_id'])
|
|
244
|
+
if receipt.exists() and read(receipt).get('raw_hash')==raw_hash:return self.admit_wallet(self.get(read(receipt)['job_id']))
|
|
222
245
|
for p in self.jobs.glob('*.json'):
|
|
223
246
|
saved=self.get(p.stem)
|
|
224
247
|
if saved.get('control_raw',{}).get(key_hash)==raw_hash or saved['key_hash']==key_hash and saved.get('raw_hash')==raw_hash:
|
|
225
248
|
fingerprint=saved.get('control_receipts',{}).get(key_hash,saved['request_hash'])
|
|
226
249
|
atomic(receipt,{'schema_version':1,'request_hash':fingerprint,'raw_hash':raw_hash,'job_id':saved['job_id']})
|
|
227
|
-
return saved
|
|
250
|
+
return self.admit_wallet(saved)
|
|
228
251
|
value = self.normalize(operation, request['input'])
|
|
229
252
|
fingerprint = digest({'operation': operation, 'input': value})
|
|
230
253
|
if receipt.exists():
|
|
231
254
|
old = read(receipt)
|
|
232
255
|
if old['request_hash'] != fingerprint: raise JobError('idempotency_conflict', 'This request key already belongs to another operation.')
|
|
233
|
-
return self.get(old['job_id'])
|
|
256
|
+
return self.admit_wallet(self.get(old['job_id']))
|
|
234
257
|
# Recover an envelope committed before its request receipt.
|
|
235
258
|
existing = next((j for p in self.jobs.glob('*.json') if (j := self.get(p.stem))['key_hash'] == key_hash or key_hash in j.get('control_receipts',{})), None)
|
|
236
259
|
if existing:
|
|
@@ -261,7 +284,10 @@ class JobStore:
|
|
|
261
284
|
'created_at': now(), 'updated_at': now(), 'sequence': 0, 'cancel_requested': False,
|
|
262
285
|
'stage': 'queued', 'events': [], 'checkpoint': 'snapshots/jobs/' + job_id,
|
|
263
286
|
'result': None, 'previous_result': previous, 'errors': [],'chains':{}}
|
|
287
|
+
if operation in ('wallet.add','wallet.refresh'):job['analysis_phase']=phase
|
|
288
|
+
if parent:job['parent_job_id']=parent
|
|
264
289
|
self.save(job)
|
|
290
|
+
self.admit_wallet(job)
|
|
265
291
|
atomic(receipt, {'schema_version': 1, 'request_hash': fingerprint,'raw_hash':raw_hash, 'job_id': job['job_id']})
|
|
266
292
|
return job
|
|
267
293
|
|
|
@@ -361,11 +387,11 @@ class JobStore:
|
|
|
361
387
|
self.save(job)
|
|
362
388
|
|
|
363
389
|
def event(self, job_id, event):
|
|
364
|
-
allowed = {'registered', 'discovery', 'chain', 'dex_discovery', 'token_images', 'published', 'failed'}
|
|
390
|
+
allowed = {'registered', 'discovery', 'chain', 'onchain_progress', 'first_evidence', 'dex_discovery', 'token_images', 'published', 'failed'}
|
|
365
391
|
if event.get('stage') not in allowed: return
|
|
366
392
|
with self.locked():
|
|
367
393
|
job = self.get(job_id)
|
|
368
|
-
counts = {k: v for k, v in event.items() if k in {'complete_chains', 'candidates', 'tokens','chain_id','registry','checked','held','block_number'} and type(v) is int and v >= 0}
|
|
394
|
+
counts = {k: v for k, v in event.items() if k in {'complete_chains', 'candidates', 'tokens','chain_id','registry','checked','held','block_number','total'} and type(v) is int and v >= 0}
|
|
369
395
|
job['stage'] = event['stage']
|
|
370
396
|
if event['stage']=='chain' and type(event.get('chain_id')) is int:
|
|
371
397
|
block=event.get('block_number');block=str(block) if type(block) is int and block>=0 else block
|
|
@@ -373,20 +399,41 @@ class JobStore:
|
|
|
373
399
|
'endpoint_index':event.get('endpoint_index') if type(event.get('endpoint_index')) is int else None,
|
|
374
400
|
'status':event.get('status') if event.get('status') in ('complete','partial','unavailable') else 'unknown'}
|
|
375
401
|
job['events'].append({'stage': event['stage'], 'observed_at': now(), 'counts': counts})
|
|
402
|
+
if event['stage']=='onchain_progress' and event.get('operation') in ('balances','registry','metadata','curves','markets'):
|
|
403
|
+
job['events'][-1]['context']={'operation':event['operation'],'chain_id':counts.get('chain_id')}
|
|
404
|
+
if event['stage']=='first_evidence' and event.get('snapshot')==job['checkpoint']+'/initial':
|
|
405
|
+
saved=self.snapshot(event['snapshot'])
|
|
406
|
+
wallet=job['input'].get('wallet') or job['input'].get('address')
|
|
407
|
+
if saved.get('wallet_address','').lower()==wallet.lower():job['result']={'snapshot_id':event['snapshot'],'status':saved['status'],'phase':'initial'}
|
|
376
408
|
if event['stage']=='chain' and type(event.get('chain_id')) is int:job['events'][-1]['context']={'chain_id':event['chain_id'],**job['chains'][str(event['chain_id'])]}
|
|
377
409
|
job['events'] = job['events'][-100:]
|
|
378
410
|
self.save(job)
|
|
379
411
|
|
|
380
412
|
def set_tags(self, job):
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
413
|
+
from wallet import registry_locked
|
|
414
|
+
with registry_locked(self.root):
|
|
415
|
+
registry = read(self.root / 'wallets.json')
|
|
416
|
+
wallet = next(w for w in registry['wallets'] if w['address_key'] == job['input']['wallet'])
|
|
417
|
+
old = wallet['tags']; tags = job['input']['tags']
|
|
418
|
+
for tag in set(old) ^ set(tags):
|
|
419
|
+
registry.setdefault('tag_history', []).append({'address_key': wallet['address_key'], 'tag': tag,
|
|
420
|
+
'action': 'add' if tag in tags else 'remove', 'observed_at': now(), 'source': 'operator'})
|
|
421
|
+
wallet['tags'] = tags; atomic(self.root / 'wallets.json', registry)
|
|
388
422
|
return {'wallet': wallet['address_key'], 'tags': tags, 'status': 'completed'}
|
|
389
423
|
|
|
424
|
+
def continue_research(self):
|
|
425
|
+
"""A durable continuation of the admitted analysis, behind initial reports."""
|
|
426
|
+
for parent in self.list():
|
|
427
|
+
if parent.get('analysis_phase')!='baseline' or parent['state'] not in ('succeeded','partial') or parent.get('cancel_requested') or parent.get('continuation_id'):continue
|
|
428
|
+
result=parent.get('result')
|
|
429
|
+
if not result or not result.get('snapshot_id'):continue
|
|
430
|
+
saved=self.snapshot(result['snapshot_id'])
|
|
431
|
+
if saved.get('pipeline',{}).get('phase')!='baseline':continue
|
|
432
|
+
wallet=parent['input'].get('address') or parent['input'].get('wallet')
|
|
433
|
+
child=self.submit({'schema_version':1,'operation':'wallet.refresh','input':{'wallet':wallet},'idempotency_key':'enrichment:'+parent['job_id']},phase='enrichment',parent=parent['job_id'])
|
|
434
|
+
with self.locked():
|
|
435
|
+
parent=self.get(parent['job_id']);parent['continuation_id']=child['job_id'];self.save(parent)
|
|
436
|
+
|
|
390
437
|
def engine_command(self, job):
|
|
391
438
|
if job['operation'] == 'prices.refresh':
|
|
392
439
|
command = [sys.executable, str(ASSETS / 'viewer/refresh_prices.py'), '--wallet', job['input']['wallet']]
|
|
@@ -394,6 +441,7 @@ class JobStore:
|
|
|
394
441
|
add = job['operation'] == 'wallet.add'
|
|
395
442
|
command = [sys.executable, str(ASSETS / 'wallet.py'), 'add' if add else 'refresh', job['input']['address'] if add else job['input']['wallet']]
|
|
396
443
|
if add: command += ['--tag', job['input']['tag']]
|
|
444
|
+
command += ['--phase',job.get('analysis_phase','enrichment')]
|
|
397
445
|
if (self.root / job['checkpoint'] / 'run.json').exists(): command += ['--resume', job['checkpoint']]
|
|
398
446
|
return command
|
|
399
447
|
|
|
@@ -412,7 +460,7 @@ class JobStore:
|
|
|
412
460
|
previous=config['rpc']['chains'].get(key,{})
|
|
413
461
|
if row['url_env']==previous.get('url_env') and previous.get('alchemy_network'):row['alchemy_network']=previous['alchemy_network']
|
|
414
462
|
config['rpc']=updated
|
|
415
|
-
elif job['operation']=='settings.discovery':config['discovery']={**
|
|
463
|
+
elif job['operation']=='settings.discovery':config['discovery']={**config['discovery'],**job['input']}
|
|
416
464
|
else:
|
|
417
465
|
from kira_config import provider_config,secret_values
|
|
418
466
|
config=provider_config(config,**job['input'])
|
|
@@ -420,12 +468,12 @@ class JobStore:
|
|
|
420
468
|
raise JobError('connection_key_missing','Kira cannot find the local Alchemy key. Follow the connection guide, then retry. Existing settings are unchanged.')
|
|
421
469
|
atomic(config_path(),config)
|
|
422
470
|
result={'status':'completed','setting':job['operation']};atomic(self.jobs/'results'/(job['job_id']+'.json'),result);return result
|
|
423
|
-
env = {**os.environ, 'KIRA_DATA_DIR': str(self.root), 'KIRA_JOB_ID': job['job_id'], 'KIRA_JOB_SNAPSHOT': job['checkpoint'], 'KIRA_ANALYSIS_FD': str(writer.fileno()), 'PYTHONUNBUFFERED': '1'}
|
|
471
|
+
env = {**os.environ, 'KIRA_DATA_DIR': str(self.root), 'KIRA_JOB_ID': job['job_id'], 'KIRA_JOB_SNAPSHOT': job['checkpoint'], 'KIRA_ANALYSIS_FD': str(writer.fileno()), 'PYTHONUNBUFFERED': '1', 'KIRA_RESEARCH_GENERATION':job.get('parent_job_id',job['job_id'])}
|
|
424
472
|
command=self.engine_command(job)
|
|
425
473
|
child = subprocess.Popen(command, env=env, cwd=ASSETS, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
|
426
474
|
start_new_session=True, pass_fds=(lease.fileno(), writer.fileno()))
|
|
427
475
|
selector = selectors.DefaultSelector(); selector.register(child.stdout, selectors.EVENT_READ)
|
|
428
|
-
buffer = b''; cancelled = False; stopped = False
|
|
476
|
+
buffer = b''; cancelled = False; stopped = False; yielded = False; next_check = 0
|
|
429
477
|
def stop_child():
|
|
430
478
|
nonlocal stopped
|
|
431
479
|
if stopped:return
|
|
@@ -445,6 +493,10 @@ class JobStore:
|
|
|
445
493
|
while child.poll() is None:
|
|
446
494
|
if self.get(job['job_id'])['cancel_requested']:
|
|
447
495
|
cancelled = True; stop_child(); break
|
|
496
|
+
if job.get('analysis_phase')=='enrichment' and time.monotonic()>=next_check:
|
|
497
|
+
next_check=time.monotonic()+2
|
|
498
|
+
if any(j['state']=='queued' and j.get('analysis_phase')=='baseline' for j in self.list()):
|
|
499
|
+
yielded=True;stop_child();break
|
|
448
500
|
for key, _ in selector.select(.2):
|
|
449
501
|
chunk = os.read(key.fileobj.fileno(), 65536)
|
|
450
502
|
if not chunk: selector.unregister(key.fileobj); continue
|
|
@@ -467,6 +519,7 @@ class JobStore:
|
|
|
467
519
|
receipt = self.publication(job)
|
|
468
520
|
if receipt: return receipt
|
|
469
521
|
if cancelled: raise JobError('cancelled', 'Job stopped. Previous published results and saved evidence were preserved.')
|
|
522
|
+
if yielded:raise JobError('yielded','Detailed research yielded to a new initial report. Saved identity checkpoints are retained.')
|
|
470
523
|
raise JobError('engine_failed', 'Analysis stopped before publication. Check connection settings and resume saved evidence.')
|
|
471
524
|
finally:
|
|
472
525
|
stop_child(); selector.close(); child.stdout.close()
|
|
@@ -478,13 +531,18 @@ class JobStore:
|
|
|
478
531
|
with (self.root / '.analysis.lock').open('a') as writer:
|
|
479
532
|
fcntl.flock(writer, fcntl.LOCK_EX)
|
|
480
533
|
self.recover()
|
|
534
|
+
self.continue_research()
|
|
481
535
|
while True:
|
|
482
536
|
writer = (self.root / '.analysis.lock').open('a')
|
|
483
537
|
fcntl.flock(writer, fcntl.LOCK_EX)
|
|
484
538
|
with self.locked():
|
|
485
|
-
queued = sorted((self.get(p.stem) for p in self.jobs.glob('*.json')), key=lambda j: j['created_at'])
|
|
539
|
+
queued = sorted((self.get(p.stem) for p in self.jobs.glob('*.json')), key=lambda j: (j.get('analysis_phase')=='enrichment',j['created_at']))
|
|
486
540
|
job = next((j for j in queued if j['state'] == 'queued'), None)
|
|
487
541
|
if not job:writer.close();return
|
|
542
|
+
if job.get('analysis_phase')=='enrichment':
|
|
543
|
+
entry=self.wallet(job['input']['wallet'])
|
|
544
|
+
if entry.get('research_generation') not in (None,job.get('parent_job_id')):
|
|
545
|
+
job.update(state='cancelled',stage='stopped',errors=[{'code':'superseded','message':'Newer holdings research replaced this detailed run. Earlier evidence is preserved.'}]);self.save(job);writer.close();continue
|
|
488
546
|
from kira_config import load_config
|
|
489
547
|
config=load_config()
|
|
490
548
|
job['provider_configuration']={'rpc_mode':config['rpc']['mode'],'discovery_provider':config['discovery']['provider'],
|
|
@@ -494,9 +552,13 @@ class JobStore:
|
|
|
494
552
|
result = self.execute(job, lease, writer)
|
|
495
553
|
with self.locked():
|
|
496
554
|
job = self.get(job['job_id']); job.update(result=result, state='succeeded' if result['status'] == 'completed' else 'partial', stage='published'); self.save(job)
|
|
555
|
+
self.continue_research()
|
|
497
556
|
except JobError as error:
|
|
498
557
|
with self.locked():
|
|
499
|
-
job = self.get(job['job_id'])
|
|
558
|
+
job = self.get(job['job_id'])
|
|
559
|
+
if error.code=='yielded':job.update(state='queued',stage='queued',errors=[])
|
|
560
|
+
else:job.update(state='cancelled' if error.code == 'cancelled' else 'failed', stage='stopped', errors=[{'code': error.code, 'message': str(error)}])
|
|
561
|
+
self.save(job)
|
|
500
562
|
except (Exception, KeyboardInterrupt):
|
|
501
563
|
with self.locked():
|
|
502
564
|
job = self.get(job['job_id'])
|
package/kira_research_tools.py
CHANGED
|
@@ -51,7 +51,7 @@ class ResearchTools:
|
|
|
51
51
|
raise JobError('job_scope','Only approved wallet research jobs are available.')
|
|
52
52
|
selector=job['input'].get('wallet') or job['input'].get('address')
|
|
53
53
|
wallet=self.wallet(selector,approved)
|
|
54
|
-
return {'wallet':wallet,**{key:job.get(key) for key in ('job_id','operation','state','stage','created_at','updated_at','checkpoint','result','chains')}}
|
|
54
|
+
return {'wallet':wallet,**{key:job.get(key) for key in ('job_id','operation','state','stage','created_at','updated_at','checkpoint','result','chains','analysis_phase','parent_job_id','continuation_id','events')}}
|
|
55
55
|
|
|
56
56
|
def call(self, name, arguments, key):
|
|
57
57
|
tool=next((row for row in CATALOG if row['name']==name),None)
|