@consensus-tools/consensus-tools 0.1.2 → 0.1.4
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 +56 -56
- package/assets/consensus-tools.png +0 -0
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -1
- package/src/cli.ts +190 -137
- package/src/config.ts +8 -5
- package/src/initWizard.ts +2 -1
- package/src/jobs/consensus.ts +116 -1
- package/src/jobs/engine.ts +75 -4
- package/src/standalone.ts +1 -1
- package/src/testing/consensusTestRunner.ts +10 -3
- package/src/types.ts +27 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# consensus.tools
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
|
|
5
5
|
**High-confidence decisions for agentic systems.**
|
|
6
6
|
Local-first. Incentive-aligned. Verifiable.
|
|
@@ -162,92 +162,86 @@ The CLI generates .sh API templates so everything is scriptable and inspectable.
|
|
|
162
162
|
|
|
163
163
|
Policies define how a job resolves. The defaults below are built-in and fully auditable.
|
|
164
164
|
|
|
165
|
-
###
|
|
165
|
+
### Active policy set (current)
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
The currently documented/default policy set is:
|
|
168
168
|
|
|
169
|
-
|
|
169
|
+
- `FIRST_SUBMISSION_WINS`
|
|
170
|
+
- `HIGHEST_CONFIDENCE_SINGLE`
|
|
171
|
+
- `APPROVAL_VOTE`
|
|
172
|
+
- `OWNER_PICK`
|
|
173
|
+
- `TRUSTED_ARBITER`
|
|
170
174
|
|
|
171
|
-
|
|
175
|
+
### Submission-oriented policies
|
|
172
176
|
|
|
173
|
-
|
|
177
|
+
#### FIRST_SUBMISSION_WINS
|
|
174
178
|
|
|
175
|
-
|
|
179
|
+
The earliest valid submission wins.
|
|
176
180
|
|
|
177
|
-
|
|
178
|
-
- All other submissions receive 0%.
|
|
179
|
-
- If no submission meets `minConfidence`, no reward is paid and funds return to the job creator (or remain unallocated, board-configurable).
|
|
181
|
+
Best for: speedrun tasks, low-ambiguity jobs, first-correct workflows.
|
|
180
182
|
|
|
181
|
-
|
|
183
|
+
Reward split (v1):
|
|
182
184
|
|
|
183
|
-
|
|
185
|
+
- 100% of reward to the first valid submission.
|
|
186
|
+
- Other submissions receive 0%.
|
|
184
187
|
|
|
185
|
-
|
|
188
|
+
#### HIGHEST_CONFIDENCE_SINGLE
|
|
186
189
|
|
|
187
|
-
|
|
190
|
+
Pick the submission with the highest declared confidence (optionally gated by `minConfidence`).
|
|
188
191
|
|
|
189
|
-
|
|
192
|
+
Best for: safety-sensitive workflows where false positives are expensive.
|
|
190
193
|
|
|
191
194
|
Reward split (v1):
|
|
192
195
|
|
|
193
|
-
- 100% of
|
|
194
|
-
-
|
|
195
|
-
-
|
|
196
|
-
|
|
197
|
-
#### TOP_K_SPLIT (K = 2 or 3)
|
|
196
|
+
- 100% of reward to the selected submission.
|
|
197
|
+
- Others receive 0%.
|
|
198
|
+
- If no submission meets threshold, no reward is paid.
|
|
198
199
|
|
|
199
|
-
|
|
200
|
+
#### OWNER_PICK
|
|
200
201
|
|
|
201
|
-
|
|
202
|
+
The job creator selects a winning submission (or none).
|
|
202
203
|
|
|
203
|
-
|
|
204
|
+
Best for: subjective/creative tasks or early HITL boards.
|
|
204
205
|
|
|
205
206
|
Reward split (v1):
|
|
206
207
|
|
|
207
|
-
-
|
|
208
|
-
-
|
|
209
|
-
- If fewer than K valid submissions exist, reward is split evenly among those selected.
|
|
210
|
-
- Submissions outside the top K receive 0%.
|
|
211
|
-
- Ordering (confidence vs score) is defined explicitly by policy config.
|
|
212
|
-
|
|
213
|
-
### Voting-mode policies
|
|
214
|
-
|
|
215
|
-
#### MAJORITY_VOTE
|
|
208
|
+
- 100% of reward to the owner-selected submission.
|
|
209
|
+
- If no winner is selected, no reward is paid.
|
|
216
210
|
|
|
217
|
-
|
|
211
|
+
#### TRUSTED_ARBITER
|
|
218
212
|
|
|
219
|
-
|
|
213
|
+
A designated arbiter resolves the job manually.
|
|
220
214
|
|
|
221
|
-
|
|
215
|
+
Best for: high-stakes workflows requiring explicit adjudication.
|
|
222
216
|
|
|
223
217
|
Reward split (v1):
|
|
224
218
|
|
|
225
|
-
-
|
|
226
|
-
-
|
|
227
|
-
- Voters who voted for a losing option receive 0%.
|
|
228
|
-
- If quorum is not met, no reward is paid and the job resolves as `NO_CONSENSUS`.
|
|
219
|
+
- 100% of reward to arbiter-selected submission.
|
|
220
|
+
- If arbiter does not select a winner, no reward is paid.
|
|
229
221
|
|
|
230
|
-
|
|
222
|
+
### Voting-oriented policy
|
|
231
223
|
|
|
232
|
-
|
|
224
|
+
#### APPROVAL_VOTE
|
|
233
225
|
|
|
234
|
-
|
|
226
|
+
Votes are tallied into per-submission scores. Settlement behavior is configurable:
|
|
235
227
|
|
|
236
|
-
|
|
228
|
+
- `immediate`: best eligible submission resolves automatically.
|
|
229
|
+
- `oracle`: vote scoring produces recommendation; oracle/manual step finalizes.
|
|
230
|
+
- `staked`: staked voting settlement path (local-first).
|
|
237
231
|
|
|
238
|
-
|
|
232
|
+
Common controls include quorum, minimum score, minimum margin, and tie-break.
|
|
233
|
+
|
|
234
|
+
Reward split (v1 default behavior):
|
|
239
235
|
|
|
240
|
-
-
|
|
241
|
-
-
|
|
242
|
-
-
|
|
243
|
-
- If quorum (by total weight or count, policy-defined) is not met, no reward is paid.
|
|
236
|
+
- Reward goes to winning submission(s) per settlement mode.
|
|
237
|
+
- Losers receive 0%.
|
|
238
|
+
- If quorum/thresholds are not met, no reward is paid.
|
|
244
239
|
|
|
245
240
|
### Important v1 design principles
|
|
246
241
|
|
|
247
|
-
-
|
|
248
|
-
- No
|
|
249
|
-
-
|
|
250
|
-
- Boards may choose to return unallocated rewards to the creator or treasury.
|
|
242
|
+
- Reward outcomes are deterministic and auditable from job + vote/submission records.
|
|
243
|
+
- No partial loser rewards by default.
|
|
244
|
+
- Boards can choose return/unallocated behavior by config.
|
|
251
245
|
|
|
252
246
|
### Editing policy defaults
|
|
253
247
|
|
|
@@ -258,7 +252,7 @@ Defaults live under:
|
|
|
258
252
|
- `local.consensusPolicies` (named policy presets)
|
|
259
253
|
- `local.jobDefaults.consensusPolicy` (fallback when no key is provided)
|
|
260
254
|
|
|
261
|
-
Use these fields to override
|
|
255
|
+
Use these fields to override defaults:
|
|
262
256
|
|
|
263
257
|
- `policyKey` to select a preset
|
|
264
258
|
- `policyConfigJson` to override fields on the preset
|
|
@@ -268,8 +262,14 @@ Example:
|
|
|
268
262
|
|
|
269
263
|
```json
|
|
270
264
|
{
|
|
271
|
-
"policyKey": "
|
|
272
|
-
"policyConfigJson": {
|
|
265
|
+
"policyKey": "APPROVAL_VOTE",
|
|
266
|
+
"policyConfigJson": {
|
|
267
|
+
"quorum": 3,
|
|
268
|
+
"minScore": 1,
|
|
269
|
+
"minMargin": 0,
|
|
270
|
+
"tieBreak": "earliest",
|
|
271
|
+
"approvalVote": { "weightMode": "equal", "settlement": "immediate" }
|
|
272
|
+
}
|
|
273
273
|
}
|
|
274
274
|
```
|
|
275
275
|
|
|
@@ -386,7 +386,7 @@ Example (see full schema in `openclaw.plugin.json`):
|
|
|
386
386
|
"maxParticipants": 3,
|
|
387
387
|
"minParticipants": 1,
|
|
388
388
|
"expiresSeconds": 86400,
|
|
389
|
-
"consensusPolicy": { "type": "
|
|
389
|
+
"consensusPolicy": { "type": "FIRST_SUBMISSION_WINS", "trustedArbiterAgentId": "", "tieBreak": "earliest" },
|
|
390
390
|
"slashingPolicy": { "enabled": false, "slashPercent": 0, "slashFlat": 0 }
|
|
391
391
|
},
|
|
392
392
|
"ledger": { "faucetEnabled": false, "initialCreditsPerAgent": 0, "balancesMode": "initial", "balances": {} }
|
|
Binary file
|
package/openclaw.plugin.json
CHANGED
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"properties": {
|
|
62
62
|
"type": {
|
|
63
63
|
"type": "string",
|
|
64
|
-
"enum": ["
|
|
65
|
-
"default": "
|
|
64
|
+
"enum": ["FIRST_SUBMISSION_WINS", "APPROVAL_VOTE", "MAJORITY_VOTE", "WEIGHTED_REPUTATION", "TRUSTED_ARBITER"],
|
|
65
|
+
"default": "FIRST_SUBMISSION_WINS"
|
|
66
66
|
},
|
|
67
67
|
"trustedArbiterAgentId": { "type": "string", "default": "" }
|
|
68
68
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@consensus-tools/consensus-tools",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"bin",
|
|
19
19
|
"index.ts",
|
|
20
20
|
"src",
|
|
21
|
+
"assets",
|
|
21
22
|
"openclaw.plugin.json",
|
|
22
23
|
"README.md"
|
|
23
24
|
],
|
package/src/cli.ts
CHANGED
|
@@ -237,16 +237,23 @@ function registerConsensusSubcommands(
|
|
|
237
237
|
.description('Cast a vote')
|
|
238
238
|
.option('--submission <id>', 'Submission id to vote for')
|
|
239
239
|
.option('--choice <key>', 'Choice key to vote for')
|
|
240
|
+
.option('--yes', 'Approval vote: yes (+1)')
|
|
241
|
+
.option('--no', 'Approval vote: no (-1)')
|
|
240
242
|
.option('--weight <n>', 'Vote weight', parseFloat)
|
|
243
|
+
.option('--stake <n>', 'Stake amount locked for this vote (staked settlement)', parseFloat)
|
|
241
244
|
.option('--json', 'JSON output')
|
|
242
245
|
.action(async (jobId: string, opts: any) => {
|
|
246
|
+
const hasYesNo = Boolean(opts.yes || opts.no);
|
|
247
|
+
const score = hasYesNo ? (opts.no ? -1 : 1) : (opts.weight ?? 1);
|
|
248
|
+
|
|
243
249
|
const vote = await backend.vote(agentId, jobId, {
|
|
244
250
|
submissionId: opts.submission,
|
|
245
251
|
choiceKey: opts.choice,
|
|
246
252
|
targetType: opts.submission ? 'SUBMISSION' : opts.choice ? 'CHOICE' : undefined,
|
|
247
253
|
targetId: opts.submission ?? opts.choice,
|
|
248
|
-
weight: opts.weight ?? 1,
|
|
249
|
-
score
|
|
254
|
+
weight: opts.weight ?? (hasYesNo ? 1 : undefined),
|
|
255
|
+
score,
|
|
256
|
+
stakeAmount: opts.stake
|
|
250
257
|
});
|
|
251
258
|
output(vote, opts.json);
|
|
252
259
|
});
|
|
@@ -582,6 +589,16 @@ function commonSh(): string {
|
|
|
582
589
|
' echo "$CONSENSUS_ROOT"',
|
|
583
590
|
'}',
|
|
584
591
|
'',
|
|
592
|
+
'local_state_file() {',
|
|
593
|
+
' # Engine-parity local state file (JsonStorage)',
|
|
594
|
+
' if [[ -n "${CONSENSUS_STATE_FILE:-}" ]]; then',
|
|
595
|
+
' echo "${CONSENSUS_STATE_FILE}"',
|
|
596
|
+
' else',
|
|
597
|
+
' echo "$(local_root)/state.json"',
|
|
598
|
+
' fi',
|
|
599
|
+
'}',
|
|
600
|
+
'',
|
|
601
|
+
'',
|
|
585
602
|
'ensure_local_board() {',
|
|
586
603
|
' local root; root="$(local_root)"',
|
|
587
604
|
' mkdir -p "$root/jobs"',
|
|
@@ -683,39 +700,35 @@ function jobsPostSh(): string {
|
|
|
683
700
|
'',
|
|
684
701
|
'POLICY="${CONSENSUS_DEFAULT_POLICY:-HIGHEST_CONFIDENCE_SINGLE}"',
|
|
685
702
|
'REWARD="${CONSENSUS_DEFAULT_REWARD:-8}"',
|
|
686
|
-
'
|
|
687
|
-
'
|
|
703
|
+
'STAKE_REQUIRED="${CONSENSUS_DEFAULT_STAKE:-4}"',
|
|
704
|
+
'EXPIRES_SECONDS="${CONSENSUS_DEFAULT_EXPIRES_SECONDS:-86400}"',
|
|
688
705
|
'MODE="$(mode)"',
|
|
689
706
|
'',
|
|
690
707
|
'if [[ "$MODE" == "local" ]]; then',
|
|
691
708
|
' ensure_local_board',
|
|
692
|
-
'
|
|
693
|
-
'',
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
'',
|
|
699
|
-
'
|
|
700
|
-
'
|
|
701
|
-
'
|
|
702
|
-
|
|
703
|
-
'
|
|
704
|
-
'
|
|
705
|
-
'
|
|
706
|
-
'
|
|
707
|
-
'
|
|
708
|
-
'
|
|
709
|
-
'
|
|
710
|
-
'
|
|
711
|
-
'
|
|
712
|
-
'
|
|
713
|
-
'
|
|
714
|
-
'
|
|
715
|
-
')"',
|
|
716
|
-
' write_json_file "$(job_file "$id")" "$job_json"',
|
|
717
|
-
' ensure_job_dir "$id"',
|
|
718
|
-
' echo "$job_json"',
|
|
709
|
+
' state_file="$(local_state_file)"',
|
|
710
|
+
' node --import tsx --input-type=module - "$state_file" "$TITLE" "$DESC" "$INPUT" "$POLICY" "$REWARD" "$STAKE_REQUIRED" "$EXPIRES_SECONDS" <<\'NODE\'',
|
|
711
|
+
"import { JsonStorage } from '@consensus-tools/consensus-tools/src/storage/JsonStorage.ts';",
|
|
712
|
+
"import { LedgerEngine } from '@consensus-tools/consensus-tools/src/ledger/ledger.ts';",
|
|
713
|
+
"import { JobEngine } from '@consensus-tools/consensus-tools/src/jobs/engine.ts';",
|
|
714
|
+
"import { defaultConfig } from '@consensus-tools/consensus-tools/src/config.ts';",
|
|
715
|
+
'const [stateFile,title,desc,input,policy,reward,stake,expires]=process.argv.slice(2);',
|
|
716
|
+
'const storage=new JsonStorage(stateFile); await storage.init();',
|
|
717
|
+
'const ledger=new LedgerEngine(storage, defaultConfig);',
|
|
718
|
+
'const engine=new JobEngine(storage, ledger, defaultConfig);',
|
|
719
|
+
"const job = await engine.postJob('cli@local', {",
|
|
720
|
+
' title,',
|
|
721
|
+
' desc,',
|
|
722
|
+
' inputRef: input,',
|
|
723
|
+
' policyKey: policy,',
|
|
724
|
+
' rewardAmount: Number(reward),',
|
|
725
|
+
' reward: Number(reward),',
|
|
726
|
+
' stakeRequired: Number(stake),',
|
|
727
|
+
' stakeAmount: Number(stake),',
|
|
728
|
+
' expiresSeconds: Number(expires)',
|
|
729
|
+
'});',
|
|
730
|
+
'console.log(JSON.stringify(job, null, 2));',
|
|
731
|
+
'NODE',
|
|
719
732
|
' exit 0',
|
|
720
733
|
'fi',
|
|
721
734
|
'',
|
|
@@ -729,14 +742,13 @@ function jobsPostSh(): string {
|
|
|
729
742
|
' "mode": "SUBMISSION",',
|
|
730
743
|
' "policyKey": "$POLICY",',
|
|
731
744
|
' "rewardAmount": $REWARD,',
|
|
732
|
-
' "stakeAmount": $
|
|
733
|
-
' "leaseSeconds": $
|
|
745
|
+
' "stakeAmount": $STAKE_REQUIRED,',
|
|
746
|
+
' "leaseSeconds": ${CONSENSUS_DEFAULT_LEASE_SECONDS:-180}',
|
|
734
747
|
'}',
|
|
735
748
|
'JSON',
|
|
736
749
|
')"',
|
|
737
750
|
'curl_json "POST" "$base/jobs" "$payload"',
|
|
738
751
|
'echo',
|
|
739
|
-
''
|
|
740
752
|
].join('\n');
|
|
741
753
|
}
|
|
742
754
|
|
|
@@ -756,14 +768,26 @@ function jobsGetSh(): string {
|
|
|
756
768
|
'',
|
|
757
769
|
'if [[ "$MODE" == "local" ]]; then',
|
|
758
770
|
' ensure_local_board',
|
|
759
|
-
'
|
|
771
|
+
' state_file="$(local_state_file)"',
|
|
772
|
+
' node --import tsx --input-type=module - "$state_file" "$JOB_ID" <<\'NODE\'',
|
|
773
|
+
"import { JsonStorage } from '@consensus-tools/consensus-tools/src/storage/JsonStorage.ts';",
|
|
774
|
+
"import { LedgerEngine } from '@consensus-tools/consensus-tools/src/ledger/ledger.ts';",
|
|
775
|
+
"import { JobEngine } from '@consensus-tools/consensus-tools/src/jobs/engine.ts';",
|
|
776
|
+
"import { defaultConfig } from '@consensus-tools/consensus-tools/src/config.ts';",
|
|
777
|
+
'const [stateFile, jobId] = process.argv.slice(2);',
|
|
778
|
+
'const storage=new JsonStorage(stateFile); await storage.init();',
|
|
779
|
+
'const ledger=new LedgerEngine(storage, defaultConfig);',
|
|
780
|
+
'const engine=new JobEngine(storage, ledger, defaultConfig);',
|
|
781
|
+
'const job = await engine.getJob(jobId);',
|
|
782
|
+
"if (!job) { console.error('Job not found'); process.exit(1); }",
|
|
783
|
+
'console.log(JSON.stringify(job, null, 2));',
|
|
784
|
+
'NODE',
|
|
760
785
|
' exit 0',
|
|
761
786
|
'fi',
|
|
762
787
|
'',
|
|
763
788
|
'base="$(remote_base)"',
|
|
764
789
|
'curl -sS "$base/jobs/$JOB_ID" -H "$(remote_auth_header)"',
|
|
765
790
|
'echo',
|
|
766
|
-
''
|
|
767
791
|
].join('\n');
|
|
768
792
|
}
|
|
769
793
|
|
|
@@ -777,13 +801,23 @@ function jobsListSh(): string {
|
|
|
777
801
|
'',
|
|
778
802
|
'if [[ "$MODE" == "local" ]]; then',
|
|
779
803
|
' ensure_local_board',
|
|
780
|
-
'
|
|
781
|
-
'
|
|
804
|
+
' state_file="$(local_state_file)"',
|
|
805
|
+
' node --import tsx --input-type=module - "$state_file" <<\'NODE\'',
|
|
806
|
+
"import { JsonStorage } from '@consensus-tools/consensus-tools/src/storage/JsonStorage.ts';",
|
|
807
|
+
"import { LedgerEngine } from '@consensus-tools/consensus-tools/src/ledger/ledger.ts';",
|
|
808
|
+
"import { JobEngine } from '@consensus-tools/consensus-tools/src/jobs/engine.ts';",
|
|
809
|
+
"import { defaultConfig } from '@consensus-tools/consensus-tools/src/config.ts';",
|
|
810
|
+
'const [stateFile]=process.argv.slice(2);',
|
|
811
|
+
'const storage=new JsonStorage(stateFile); await storage.init();',
|
|
812
|
+
'const ledger=new LedgerEngine(storage, defaultConfig);',
|
|
813
|
+
'const engine=new JobEngine(storage, ledger, defaultConfig);',
|
|
814
|
+
'const jobs = await engine.listJobs({});',
|
|
815
|
+
'for (const j of jobs) console.log(j.id);',
|
|
816
|
+
'NODE',
|
|
782
817
|
' exit 0',
|
|
783
818
|
'fi',
|
|
784
819
|
'',
|
|
785
820
|
'base="$(remote_base)"',
|
|
786
|
-
'# Optional: pass query string as $1, e.g. "status=OPEN&mode=SUBMISSION"',
|
|
787
821
|
'QS="${1:-}"',
|
|
788
822
|
'url="$base/jobs"',
|
|
789
823
|
'if [[ -n "$QS" ]]; then',
|
|
@@ -791,7 +825,6 @@ function jobsListSh(): string {
|
|
|
791
825
|
'fi',
|
|
792
826
|
'curl -sS "$url" -H "$(remote_auth_header)"',
|
|
793
827
|
'echo',
|
|
794
|
-
''
|
|
795
828
|
].join('\n');
|
|
796
829
|
}
|
|
797
830
|
|
|
@@ -804,10 +837,10 @@ function submissionsCreateSh(): string {
|
|
|
804
837
|
'JOB_ID="${1:-}"',
|
|
805
838
|
'ARTIFACT_JSON="${2:-}"',
|
|
806
839
|
'SUMMARY="${3:-}"',
|
|
840
|
+
'CONFIDENCE="${4:-0.5}"',
|
|
807
841
|
'',
|
|
808
842
|
'if [[ -z "$JOB_ID" || -z "$ARTIFACT_JSON" ]]; then',
|
|
809
|
-
' echo "Usage: submissions_create.sh <jobId> <artifact_json> [summary]" >&2',
|
|
810
|
-
' echo "Example: submissions_create.sh job_... {\\\"toxic\\\":false,\\\"confidence\\\":0.98,\\\"brief_reason\\\":\\\"...\\\"}" >&2',
|
|
843
|
+
' echo "Usage: submissions_create.sh <jobId> <artifact_json> [summary] [confidence]" >&2',
|
|
811
844
|
' exit 2',
|
|
812
845
|
'fi',
|
|
813
846
|
'',
|
|
@@ -815,23 +848,20 @@ function submissionsCreateSh(): string {
|
|
|
815
848
|
'',
|
|
816
849
|
'if [[ "$MODE" == "local" ]]; then',
|
|
817
850
|
' ensure_local_board',
|
|
818
|
-
'
|
|
819
|
-
'',
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
'
|
|
825
|
-
'
|
|
826
|
-
'
|
|
827
|
-
'
|
|
828
|
-
'
|
|
829
|
-
'
|
|
830
|
-
'
|
|
831
|
-
'
|
|
832
|
-
')"',
|
|
833
|
-
' write_json_file "$(job_dir "$JOB_ID")/submissions/${sid}.json" "$sub_json"',
|
|
834
|
-
' echo "$sub_json"',
|
|
851
|
+
' state_file="$(local_state_file)"',
|
|
852
|
+
' node --import tsx --input-type=module - "$state_file" "$JOB_ID" "$ARTIFACT_JSON" "${SUMMARY:-}" "$CONFIDENCE" <<\'NODE\'',
|
|
853
|
+
"import { JsonStorage } from '@consensus-tools/consensus-tools/src/storage/JsonStorage.ts';",
|
|
854
|
+
"import { LedgerEngine } from '@consensus-tools/consensus-tools/src/ledger/ledger.ts';",
|
|
855
|
+
"import { JobEngine } from '@consensus-tools/consensus-tools/src/jobs/engine.ts';",
|
|
856
|
+
"import { defaultConfig } from '@consensus-tools/consensus-tools/src/config.ts';",
|
|
857
|
+
'const [stateFile, jobId, artifactJson, summary, conf] = process.argv.slice(2);',
|
|
858
|
+
'const artifacts = JSON.parse(artifactJson);',
|
|
859
|
+
'const storage=new JsonStorage(stateFile); await storage.init();',
|
|
860
|
+
'const ledger=new LedgerEngine(storage, defaultConfig);',
|
|
861
|
+
'const engine=new JobEngine(storage, ledger, defaultConfig);',
|
|
862
|
+
"const sub = await engine.submitJob('cli@local', jobId, { summary: summary || '', artifacts, confidence: Number(conf) });",
|
|
863
|
+
'console.log(JSON.stringify(sub, null, 2));',
|
|
864
|
+
'NODE',
|
|
835
865
|
' exit 0',
|
|
836
866
|
'fi',
|
|
837
867
|
'',
|
|
@@ -845,7 +875,6 @@ function submissionsCreateSh(): string {
|
|
|
845
875
|
')"',
|
|
846
876
|
'curl_json "POST" "$base/jobs/$JOB_ID/submissions" "$payload"',
|
|
847
877
|
'echo',
|
|
848
|
-
''
|
|
849
878
|
].join('\n');
|
|
850
879
|
}
|
|
851
880
|
|
|
@@ -865,15 +894,25 @@ function submissionsListSh(): string {
|
|
|
865
894
|
'',
|
|
866
895
|
'if [[ "$MODE" == "local" ]]; then',
|
|
867
896
|
' ensure_local_board',
|
|
868
|
-
'
|
|
869
|
-
'
|
|
897
|
+
' state_file="$(local_state_file)"',
|
|
898
|
+
' node --import tsx --input-type=module - "$state_file" "$JOB_ID" <<\'NODE\'',
|
|
899
|
+
"import { JsonStorage } from '@consensus-tools/consensus-tools/src/storage/JsonStorage.ts';",
|
|
900
|
+
"import { LedgerEngine } from '@consensus-tools/consensus-tools/src/ledger/ledger.ts';",
|
|
901
|
+
"import { JobEngine } from '@consensus-tools/consensus-tools/src/jobs/engine.ts';",
|
|
902
|
+
"import { defaultConfig } from '@consensus-tools/consensus-tools/src/config.ts';",
|
|
903
|
+
'const [stateFile, jobId] = process.argv.slice(2);',
|
|
904
|
+
'const storage=new JsonStorage(stateFile); await storage.init();',
|
|
905
|
+
'const ledger=new LedgerEngine(storage, defaultConfig);',
|
|
906
|
+
'const engine=new JobEngine(storage, ledger, defaultConfig);',
|
|
907
|
+
'const list = await engine.listSubmissions(jobId);',
|
|
908
|
+
'for (const s of list) console.log(JSON.stringify(s, null, 2));',
|
|
909
|
+
'NODE',
|
|
870
910
|
' exit 0',
|
|
871
911
|
'fi',
|
|
872
912
|
'',
|
|
873
913
|
'base="$(remote_base)"',
|
|
874
914
|
'curl -sS "$base/jobs/$JOB_ID/submissions" -H "$(remote_auth_header)"',
|
|
875
915
|
'echo',
|
|
876
|
-
''
|
|
877
916
|
].join('\n');
|
|
878
917
|
}
|
|
879
918
|
|
|
@@ -884,14 +923,27 @@ function votesCastSh(): string {
|
|
|
884
923
|
'source "$(dirname "$0")/common.sh"',
|
|
885
924
|
'',
|
|
886
925
|
'JOB_ID="${1:-}"',
|
|
887
|
-
'
|
|
888
|
-
'
|
|
889
|
-
'
|
|
926
|
+
'shift || true',
|
|
927
|
+
'TARGET_TYPE=""',
|
|
928
|
+
'TARGET_ID=""',
|
|
929
|
+
'WEIGHT="1"',
|
|
930
|
+
'',
|
|
931
|
+
'if [[ "${1:-}" == "SUBMISSION" || "${1:-}" == "CHOICE" ]]; then',
|
|
932
|
+
' TARGET_TYPE="$1"; TARGET_ID="${2:-}"; WEIGHT="${3:-1}"',
|
|
933
|
+
'else',
|
|
934
|
+
' while [[ $# -gt 0 ]]; do',
|
|
935
|
+
' case "$1" in',
|
|
936
|
+
' --submission) TARGET_TYPE="SUBMISSION"; TARGET_ID="${2:-}"; shift 2 ;;',
|
|
937
|
+
' --choice) TARGET_TYPE="CHOICE"; TARGET_ID="${2:-}"; shift 2 ;;',
|
|
938
|
+
' --weight) WEIGHT="${2:-1}"; shift 2 ;;',
|
|
939
|
+
' *) echo "Unknown arg: $1" >&2; exit 2 ;;',
|
|
940
|
+
' esac',
|
|
941
|
+
' done',
|
|
942
|
+
'fi',
|
|
890
943
|
'',
|
|
891
944
|
'if [[ -z "$JOB_ID" || -z "$TARGET_TYPE" || -z "$TARGET_ID" ]]; then',
|
|
892
|
-
' echo "Usage: votes_cast.sh <jobId>
|
|
893
|
-
' echo "
|
|
894
|
-
' echo "Example: votes_cast.sh job_... CHOICE TOXIC_FALSE 1" >&2',
|
|
945
|
+
' echo "Usage: votes_cast.sh <jobId> SUBMISSION <submissionId> [weight]" >&2',
|
|
946
|
+
' echo " or: votes_cast.sh <jobId> --submission <id> [--weight <n>]" >&2',
|
|
895
947
|
' exit 2',
|
|
896
948
|
'fi',
|
|
897
949
|
'',
|
|
@@ -899,22 +951,19 @@ function votesCastSh(): string {
|
|
|
899
951
|
'',
|
|
900
952
|
'if [[ "$MODE" == "local" ]]; then',
|
|
901
953
|
' ensure_local_board',
|
|
902
|
-
'
|
|
903
|
-
'',
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
'
|
|
909
|
-
'
|
|
910
|
-
'
|
|
911
|
-
'
|
|
912
|
-
'
|
|
913
|
-
'
|
|
914
|
-
'
|
|
915
|
-
')"',
|
|
916
|
-
' write_json_file "$(job_dir "$JOB_ID")/votes/${vid}.json" "$vote_json"',
|
|
917
|
-
' echo "$vote_json"',
|
|
954
|
+
' state_file="$(local_state_file)"',
|
|
955
|
+
' node --import tsx --input-type=module - "$state_file" "$JOB_ID" "$TARGET_TYPE" "$TARGET_ID" "$WEIGHT" <<\'NODE\'',
|
|
956
|
+
"import { JsonStorage } from '@consensus-tools/consensus-tools/src/storage/JsonStorage.ts';",
|
|
957
|
+
"import { LedgerEngine } from '@consensus-tools/consensus-tools/src/ledger/ledger.ts';",
|
|
958
|
+
"import { JobEngine } from '@consensus-tools/consensus-tools/src/jobs/engine.ts';",
|
|
959
|
+
"import { defaultConfig } from '@consensus-tools/consensus-tools/src/config.ts';",
|
|
960
|
+
'const [stateFile, jobId, targetType, targetId, weight] = process.argv.slice(2);',
|
|
961
|
+
'const storage=new JsonStorage(stateFile); await storage.init();',
|
|
962
|
+
'const ledger=new LedgerEngine(storage, defaultConfig);',
|
|
963
|
+
'const engine=new JobEngine(storage, ledger, defaultConfig);',
|
|
964
|
+
"const vote = await engine.vote('cli@local', jobId, { targetType, targetId, submissionId: targetType==='SUBMISSION'?targetId:undefined, choiceKey: targetType==='CHOICE'?targetId:undefined, weight: Number(weight), score: Number(weight) });",
|
|
965
|
+
'console.log(JSON.stringify(vote, null, 2));',
|
|
966
|
+
'NODE',
|
|
918
967
|
' exit 0',
|
|
919
968
|
'fi',
|
|
920
969
|
'',
|
|
@@ -929,7 +978,6 @@ function votesCastSh(): string {
|
|
|
929
978
|
')"',
|
|
930
979
|
'curl_json "POST" "$base/jobs/$JOB_ID/votes" "$payload"',
|
|
931
980
|
'echo',
|
|
932
|
-
''
|
|
933
981
|
].join('\n');
|
|
934
982
|
}
|
|
935
983
|
|
|
@@ -949,15 +997,25 @@ function votesListSh(): string {
|
|
|
949
997
|
'',
|
|
950
998
|
'if [[ "$MODE" == "local" ]]; then',
|
|
951
999
|
' ensure_local_board',
|
|
952
|
-
'
|
|
953
|
-
'
|
|
1000
|
+
' state_file="$(local_state_file)"',
|
|
1001
|
+
' node --import tsx --input-type=module - "$state_file" "$JOB_ID" <<\'NODE\'',
|
|
1002
|
+
"import { JsonStorage } from '@consensus-tools/consensus-tools/src/storage/JsonStorage.ts';",
|
|
1003
|
+
"import { LedgerEngine } from '@consensus-tools/consensus-tools/src/ledger/ledger.ts';",
|
|
1004
|
+
"import { JobEngine } from '@consensus-tools/consensus-tools/src/jobs/engine.ts';",
|
|
1005
|
+
"import { defaultConfig } from '@consensus-tools/consensus-tools/src/config.ts';",
|
|
1006
|
+
'const [stateFile, jobId] = process.argv.slice(2);',
|
|
1007
|
+
'const storage=new JsonStorage(stateFile); await storage.init();',
|
|
1008
|
+
'const ledger=new LedgerEngine(storage, defaultConfig);',
|
|
1009
|
+
'const engine=new JobEngine(storage, ledger, defaultConfig);',
|
|
1010
|
+
'const list = await engine.listVotes(jobId);',
|
|
1011
|
+
'for (const v of list) console.log(JSON.stringify(v, null, 2));',
|
|
1012
|
+
'NODE',
|
|
954
1013
|
' exit 0',
|
|
955
1014
|
'fi',
|
|
956
1015
|
'',
|
|
957
1016
|
'base="$(remote_base)"',
|
|
958
1017
|
'curl -sS "$base/jobs/$JOB_ID/votes" -H "$(remote_auth_header)"',
|
|
959
1018
|
'echo',
|
|
960
|
-
''
|
|
961
1019
|
].join('\n');
|
|
962
1020
|
}
|
|
963
1021
|
|
|
@@ -968,8 +1026,20 @@ function resolveSh(): string {
|
|
|
968
1026
|
'source "$(dirname "$0")/common.sh"',
|
|
969
1027
|
'',
|
|
970
1028
|
'JOB_ID="${1:-}"',
|
|
1029
|
+
'shift || true',
|
|
1030
|
+
'MANUAL_WINNER=""',
|
|
1031
|
+
'MANUAL_SUB=""',
|
|
1032
|
+
'',
|
|
1033
|
+
'while [[ $# -gt 0 ]]; do',
|
|
1034
|
+
' case "$1" in',
|
|
1035
|
+
' --winner) MANUAL_WINNER="${2:-}"; shift 2 ;;',
|
|
1036
|
+
' --submission) MANUAL_SUB="${2:-}"; shift 2 ;;',
|
|
1037
|
+
' *) echo "Unknown arg: $1" >&2; exit 2 ;;',
|
|
1038
|
+
' esac',
|
|
1039
|
+
'done',
|
|
1040
|
+
'',
|
|
971
1041
|
'if [[ -z "$JOB_ID" ]]; then',
|
|
972
|
-
' echo "Usage: resolve.sh <jobId>" >&2',
|
|
1042
|
+
' echo "Usage: resolve.sh <jobId> [--winner <agentId>] [--submission <submissionId>]" >&2',
|
|
973
1043
|
' exit 2',
|
|
974
1044
|
'fi',
|
|
975
1045
|
'',
|
|
@@ -977,56 +1047,29 @@ function resolveSh(): string {
|
|
|
977
1047
|
'',
|
|
978
1048
|
'if [[ "$MODE" == "local" ]]; then',
|
|
979
1049
|
' ensure_local_board',
|
|
980
|
-
'
|
|
981
|
-
'',
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
'',
|
|
986
|
-
'
|
|
987
|
-
'
|
|
988
|
-
'
|
|
989
|
-
'
|
|
990
|
-
'
|
|
991
|
-
'',
|
|
992
|
-
'
|
|
993
|
-
|
|
994
|
-
'
|
|
995
|
-
'
|
|
996
|
-
'
|
|
997
|
-
' with open(p,"r") as f:',
|
|
998
|
-
' s=json.load(f)',
|
|
999
|
-
' conf=None',
|
|
1000
|
-
' try:',
|
|
1001
|
-
' conf=float(s.get("artifact",{}).get("confidence"))',
|
|
1002
|
-
' except Exception:',
|
|
1003
|
-
' conf=None',
|
|
1004
|
-
' subs.append((conf,s.get("createdAt",""),s,p))',
|
|
1005
|
-
'# sort: confidence desc (None last), then createdAt desc',
|
|
1006
|
-
'def key(t):',
|
|
1007
|
-
' conf,created,_,_ = t',
|
|
1008
|
-
' return (conf is not None, conf if conf is not None else -1.0, created)',
|
|
1009
|
-
'subs_sorted=sorted(subs, key=key, reverse=True)',
|
|
1010
|
-
'conf,created,s,p=subs_sorted[0]',
|
|
1011
|
-
'result={',
|
|
1012
|
-
' "jobId": job_id,',
|
|
1013
|
-
' "mode": "SUBMISSION",',
|
|
1014
|
-
' "selectedSubmissionId": s.get("id"),',
|
|
1015
|
-
' "selectedSubmissionPath": p,',
|
|
1016
|
-
' "resolvedAt": __import__("datetime").datetime.utcnow().isoformat()+"Z",',
|
|
1017
|
-
' "artifact": s.get("artifact"),',
|
|
1018
|
-
' "summary": s.get("summary","")',
|
|
1019
|
-
'}',
|
|
1020
|
-
'print(json.dumps(result, indent=2))',
|
|
1021
|
-
'PY',
|
|
1022
|
-
'',
|
|
1050
|
+
' state_file="$(local_state_file)"',
|
|
1051
|
+
' node --import tsx --input-type=module - "$state_file" "$JOB_ID" "$MANUAL_WINNER" "$MANUAL_SUB" <<\'NODE\'',
|
|
1052
|
+
"import { JsonStorage } from '@consensus-tools/consensus-tools/src/storage/JsonStorage.ts';",
|
|
1053
|
+
"import { LedgerEngine } from '@consensus-tools/consensus-tools/src/ledger/ledger.ts';",
|
|
1054
|
+
"import { JobEngine } from '@consensus-tools/consensus-tools/src/jobs/engine.ts';",
|
|
1055
|
+
"import { defaultConfig } from '@consensus-tools/consensus-tools/src/config.ts';",
|
|
1056
|
+
'const [stateFile, jobId, winner, subId] = process.argv.slice(2);',
|
|
1057
|
+
'const storage=new JsonStorage(stateFile); await storage.init();',
|
|
1058
|
+
'const ledger=new LedgerEngine(storage, defaultConfig);',
|
|
1059
|
+
'const engine=new JobEngine(storage, ledger, defaultConfig);',
|
|
1060
|
+
'const input:any = {};',
|
|
1061
|
+
'if (winner) input.manualWinners = [winner];',
|
|
1062
|
+
'if (subId) input.manualSubmissionId = subId;',
|
|
1063
|
+
"const actor = winner || 'cli@local';",
|
|
1064
|
+
'const res = await engine.resolveJob(actor, jobId, input);',
|
|
1065
|
+
'console.log(JSON.stringify(res, null, 2));',
|
|
1066
|
+
'NODE',
|
|
1023
1067
|
' exit 0',
|
|
1024
1068
|
'fi',
|
|
1025
1069
|
'',
|
|
1026
1070
|
'base="$(remote_base)"',
|
|
1027
1071
|
'curl -sS -X POST "$base/jobs/$JOB_ID/resolve" -H "$(remote_auth_header)"',
|
|
1028
1072
|
'echo',
|
|
1029
|
-
''
|
|
1030
1073
|
].join('\n');
|
|
1031
1074
|
}
|
|
1032
1075
|
|
|
@@ -1046,14 +1089,24 @@ function resultGetSh(): string {
|
|
|
1046
1089
|
'',
|
|
1047
1090
|
'if [[ "$MODE" == "local" ]]; then',
|
|
1048
1091
|
' ensure_local_board',
|
|
1049
|
-
'
|
|
1050
|
-
'
|
|
1092
|
+
' state_file="$(local_state_file)"',
|
|
1093
|
+
' node --import tsx --input-type=module - "$state_file" "$JOB_ID" <<\'NODE\'',
|
|
1094
|
+
"import { JsonStorage } from '@consensus-tools/consensus-tools/src/storage/JsonStorage.ts';",
|
|
1095
|
+
"import { LedgerEngine } from '@consensus-tools/consensus-tools/src/ledger/ledger.ts';",
|
|
1096
|
+
"import { JobEngine } from '@consensus-tools/consensus-tools/src/jobs/engine.ts';",
|
|
1097
|
+
"import { defaultConfig } from '@consensus-tools/consensus-tools/src/config.ts';",
|
|
1098
|
+
'const [stateFile, jobId] = process.argv.slice(2);',
|
|
1099
|
+
'const storage=new JsonStorage(stateFile); await storage.init();',
|
|
1100
|
+
'const ledger=new LedgerEngine(storage, defaultConfig);',
|
|
1101
|
+
'const engine=new JobEngine(storage, ledger, defaultConfig);',
|
|
1102
|
+
'const status = await engine.getStatus(jobId);',
|
|
1103
|
+
'console.log(JSON.stringify(status.resolution ?? null, null, 2));',
|
|
1104
|
+
'NODE',
|
|
1051
1105
|
' exit 0',
|
|
1052
1106
|
'fi',
|
|
1053
1107
|
'',
|
|
1054
1108
|
'base="$(remote_base)"',
|
|
1055
1109
|
'curl -sS "$base/jobs/$JOB_ID/result" -H "$(remote_auth_header)"',
|
|
1056
1110
|
'echo',
|
|
1057
|
-
''
|
|
1058
1111
|
].join('\n');
|
|
1059
1112
|
}
|
package/src/config.ts
CHANGED
|
@@ -50,8 +50,9 @@ export const configSchema = {
|
|
|
50
50
|
type: {
|
|
51
51
|
type: 'string',
|
|
52
52
|
enum: [
|
|
53
|
-
'
|
|
53
|
+
'FIRST_SUBMISSION_WINS',
|
|
54
54
|
'HIGHEST_CONFIDENCE_SINGLE',
|
|
55
|
+
'APPROVAL_VOTE',
|
|
55
56
|
'OWNER_PICK',
|
|
56
57
|
'TOP_K_SPLIT',
|
|
57
58
|
'MAJORITY_VOTE',
|
|
@@ -59,7 +60,7 @@ export const configSchema = {
|
|
|
59
60
|
'WEIGHTED_REPUTATION',
|
|
60
61
|
'TRUSTED_ARBITER'
|
|
61
62
|
],
|
|
62
|
-
default: '
|
|
63
|
+
default: 'FIRST_SUBMISSION_WINS'
|
|
63
64
|
},
|
|
64
65
|
trustedArbiterAgentId: { type: 'string', default: '' },
|
|
65
66
|
minConfidence: { type: 'number', default: 0, minimum: 0, maximum: 1 },
|
|
@@ -99,8 +100,9 @@ export const configSchema = {
|
|
|
99
100
|
type: {
|
|
100
101
|
type: 'string',
|
|
101
102
|
enum: [
|
|
102
|
-
'
|
|
103
|
+
'FIRST_SUBMISSION_WINS',
|
|
103
104
|
'HIGHEST_CONFIDENCE_SINGLE',
|
|
105
|
+
'APPROVAL_VOTE',
|
|
104
106
|
'OWNER_PICK',
|
|
105
107
|
'TOP_K_SPLIT',
|
|
106
108
|
'MAJORITY_VOTE',
|
|
@@ -190,12 +192,13 @@ export const defaultConfig: ConsensusToolsConfig = {
|
|
|
190
192
|
maxParticipants: 3,
|
|
191
193
|
minParticipants: 1,
|
|
192
194
|
expiresSeconds: 86400,
|
|
193
|
-
consensusPolicy: { type: '
|
|
195
|
+
consensusPolicy: { type: 'FIRST_SUBMISSION_WINS', trustedArbiterAgentId: '', tieBreak: 'earliest' },
|
|
194
196
|
slashingPolicy: { enabled: false, slashPercent: 0, slashFlat: 0 }
|
|
195
197
|
},
|
|
196
198
|
consensusPolicies: {
|
|
197
|
-
|
|
199
|
+
FIRST_SUBMISSION_WINS: { type: 'FIRST_SUBMISSION_WINS' },
|
|
198
200
|
HIGHEST_CONFIDENCE_SINGLE: { type: 'HIGHEST_CONFIDENCE_SINGLE', minConfidence: 0 },
|
|
201
|
+
APPROVAL_VOTE: { type: 'APPROVAL_VOTE', quorum: 1, minScore: 1, minMargin: 0, tieBreak: 'earliest', approvalVote: { weightMode: 'equal', settlement: 'immediate' } },
|
|
199
202
|
OWNER_PICK: { type: 'OWNER_PICK' },
|
|
200
203
|
TOP_K_SPLIT: { type: 'TOP_K_SPLIT', topK: 2, ordering: 'confidence' },
|
|
201
204
|
MAJORITY_VOTE: { type: 'MAJORITY_VOTE' },
|
package/src/initWizard.ts
CHANGED
|
@@ -12,9 +12,10 @@ export type InitWizardResult = {
|
|
|
12
12
|
|
|
13
13
|
const POLICY_CHOICES = [
|
|
14
14
|
'HIGHEST_CONFIDENCE_SINGLE',
|
|
15
|
+
'APPROVAL_VOTE',
|
|
15
16
|
'TOP_K_SPLIT',
|
|
16
17
|
'OWNER_PICK',
|
|
17
|
-
'
|
|
18
|
+
'FIRST_SUBMISSION_WINS',
|
|
18
19
|
'MAJORITY_VOTE',
|
|
19
20
|
'WEIGHTED_VOTE_SIMPLE',
|
|
20
21
|
'WEIGHTED_REPUTATION',
|
package/src/jobs/consensus.ts
CHANGED
|
@@ -56,7 +56,7 @@ export function resolveConsensus(input: ConsensusInput): ConsensusResult {
|
|
|
56
56
|
return { winners: [], winningSubmissionIds: [], consensusTrace: { policy, reason: 'no_submissions' }, finalArtifact: null };
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
if (policy === '
|
|
59
|
+
if (policy === 'FIRST_SUBMISSION_WINS') {
|
|
60
60
|
const sorted = [...input.submissions].sort((a, b) => Date.parse(a.submittedAt) - Date.parse(b.submittedAt));
|
|
61
61
|
const winner = sorted[0];
|
|
62
62
|
return {
|
|
@@ -67,6 +67,121 @@ export function resolveConsensus(input: ConsensusInput): ConsensusResult {
|
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
if (policy === 'APPROVAL_VOTE') {
|
|
71
|
+
const quorum = input.job.consensusPolicy.quorum;
|
|
72
|
+
const minScore = input.job.consensusPolicy.minScore ?? 1;
|
|
73
|
+
const minMargin = input.job.consensusPolicy.minMargin ?? 0;
|
|
74
|
+
const tieBreak = input.job.consensusPolicy.tieBreak ?? 'earliest';
|
|
75
|
+
|
|
76
|
+
const weightMode = input.job.consensusPolicy.approvalVote?.weightMode ?? 'equal';
|
|
77
|
+
const settlement = input.job.consensusPolicy.approvalVote?.settlement ?? 'immediate';
|
|
78
|
+
|
|
79
|
+
// Oracle settlement can always be manually finalized by the arbiter, even with no votes.
|
|
80
|
+
if (settlement === 'oracle' && input.manualWinnerAgentIds && input.manualWinnerAgentIds.length) {
|
|
81
|
+
return {
|
|
82
|
+
winners: input.manualWinnerAgentIds,
|
|
83
|
+
winningSubmissionIds: input.manualSubmissionId ? [input.manualSubmissionId] : [],
|
|
84
|
+
consensusTrace: { policy, settlement, mode: 'manual' },
|
|
85
|
+
finalArtifact: findArtifact(input, input.manualSubmissionId)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const scores: Record<string, number> = {};
|
|
90
|
+
const voteCounts: Record<string, number> = {};
|
|
91
|
+
|
|
92
|
+
// Only consider votes that target submissions.
|
|
93
|
+
const votes = input.votes.filter((v) => v.submissionId || (v.targetType === 'SUBMISSION' && v.targetId));
|
|
94
|
+
if (quorum && votes.length < quorum) {
|
|
95
|
+
return {
|
|
96
|
+
winners: [],
|
|
97
|
+
winningSubmissionIds: [],
|
|
98
|
+
consensusTrace: { policy, settlement, reason: 'quorum_not_met', quorum, votes: votes.length },
|
|
99
|
+
finalArtifact: null
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const vote of votes) {
|
|
104
|
+
const sid = vote.submissionId ?? (vote.targetType === 'SUBMISSION' ? vote.targetId : undefined);
|
|
105
|
+
if (!sid) continue;
|
|
106
|
+
|
|
107
|
+
let weight = 1;
|
|
108
|
+
if (weightMode === 'explicit') weight = vote.weight ?? 1;
|
|
109
|
+
if (weightMode === 'reputation') weight = input.reputation(vote.agentId);
|
|
110
|
+
|
|
111
|
+
// score should be +1 (YES) or -1 (NO); clamp to [-1,1] to avoid weirdness.
|
|
112
|
+
const s = Math.max(-1, Math.min(1, vote.score ?? 0));
|
|
113
|
+
scores[sid] = (scores[sid] || 0) + s * weight;
|
|
114
|
+
voteCounts[sid] = (voteCounts[sid] || 0) + 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// rank submissions by score desc
|
|
118
|
+
const ranked = input.submissions
|
|
119
|
+
.map((sub) => ({ sub, score: scores[sub.id] || 0, votes: voteCounts[sub.id] || 0 }))
|
|
120
|
+
.sort((a, b) => {
|
|
121
|
+
if (b.score === a.score) {
|
|
122
|
+
if (tieBreak === 'confidence') return b.sub.confidence - a.sub.confidence;
|
|
123
|
+
// default earliest
|
|
124
|
+
return Date.parse(a.sub.submittedAt) - Date.parse(b.sub.submittedAt);
|
|
125
|
+
}
|
|
126
|
+
return b.score - a.score;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const best = ranked[0];
|
|
130
|
+
const second = ranked[1];
|
|
131
|
+
const margin = second ? best.score - second.score : best.score;
|
|
132
|
+
|
|
133
|
+
if (!best || best.votes === 0) {
|
|
134
|
+
return {
|
|
135
|
+
winners: [],
|
|
136
|
+
winningSubmissionIds: [],
|
|
137
|
+
consensusTrace: { policy, settlement, reason: 'no_votes', scores, voteCounts },
|
|
138
|
+
finalArtifact: null
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (best.score < minScore || margin < minMargin) {
|
|
143
|
+
return {
|
|
144
|
+
winners: [],
|
|
145
|
+
winningSubmissionIds: [],
|
|
146
|
+
consensusTrace: { policy, settlement, reason: 'threshold_not_met', minScore, minMargin, best: best.score, margin, scores, voteCounts },
|
|
147
|
+
finalArtifact: null
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (settlement === 'oracle' || tieBreak === 'arbiter') {
|
|
152
|
+
// Oracle / arbiter settlement: allow manual finalization, otherwise provide a recommendation.
|
|
153
|
+
if (input.manualWinnerAgentIds && input.manualWinnerAgentIds.length) {
|
|
154
|
+
return {
|
|
155
|
+
winners: input.manualWinnerAgentIds,
|
|
156
|
+
winningSubmissionIds: input.manualSubmissionId ? [input.manualSubmissionId] : [],
|
|
157
|
+
consensusTrace: {
|
|
158
|
+
policy,
|
|
159
|
+
settlement,
|
|
160
|
+
mode: 'manual',
|
|
161
|
+
recommendedSubmissionId: best.sub.id,
|
|
162
|
+
recommendedAgentId: best.sub.agentId,
|
|
163
|
+
scores,
|
|
164
|
+
voteCounts
|
|
165
|
+
},
|
|
166
|
+
finalArtifact: findArtifact(input, input.manualSubmissionId)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
winners: [],
|
|
171
|
+
winningSubmissionIds: [],
|
|
172
|
+
consensusTrace: { policy, settlement, mode: 'awaiting_oracle', recommendedSubmissionId: best.sub.id, recommendedAgentId: best.sub.agentId, scores, voteCounts },
|
|
173
|
+
finalArtifact: null
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
winners: [best.sub.agentId],
|
|
179
|
+
winningSubmissionIds: [best.sub.id],
|
|
180
|
+
consensusTrace: { policy, settlement, scores, voteCounts, minScore, minMargin, tieBreak },
|
|
181
|
+
finalArtifact: best.sub.artifacts
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
70
185
|
if (policy === 'HIGHEST_CONFIDENCE_SINGLE') {
|
|
71
186
|
const minConfidence = input.job.consensusPolicy.minConfidence ?? 0;
|
|
72
187
|
const sorted = [...input.submissions]
|
package/src/jobs/engine.ts
CHANGED
|
@@ -297,11 +297,13 @@ export class JobEngine {
|
|
|
297
297
|
async vote(agentId: string, jobId: string, input: VoteInput): Promise<Vote> {
|
|
298
298
|
const now = nowIso();
|
|
299
299
|
return (await this.storage.update((state) => {
|
|
300
|
+
// optional: stake on votes (APPROVAL_VOTE staked settlement)
|
|
301
|
+
const stakeAmount = input.stakeAmount ? Math.max(0, Number(input.stakeAmount)) : 0;
|
|
300
302
|
const job = state.jobs.find((j) => j.id === jobId);
|
|
301
303
|
if (!job) throw new Error(`Job not found: ${jobId}`);
|
|
302
304
|
if (
|
|
303
305
|
job.mode === 'SUBMISSION' &&
|
|
304
|
-
(job.consensusPolicy.type === '
|
|
306
|
+
(job.consensusPolicy.type === 'FIRST_SUBMISSION_WINS' ||
|
|
305
307
|
job.consensusPolicy.type === 'HIGHEST_CONFIDENCE_SINGLE' ||
|
|
306
308
|
job.consensusPolicy.type === 'OWNER_PICK' ||
|
|
307
309
|
job.consensusPolicy.type === 'TOP_K_SPLIT' ||
|
|
@@ -309,6 +311,22 @@ export class JobEngine {
|
|
|
309
311
|
) {
|
|
310
312
|
throw new Error('Voting not enabled for this job');
|
|
311
313
|
}
|
|
314
|
+
|
|
315
|
+
// optional vote stake (only meaningful for APPROVAL_VOTE settlement=staked)
|
|
316
|
+
if (stakeAmount > 0) {
|
|
317
|
+
const currentBalance = getBalance(state.ledger, agentId);
|
|
318
|
+
const nextBalance = currentBalance - Math.abs(stakeAmount);
|
|
319
|
+
ensureNonNegative(nextBalance, `${agentId} vote stake for ${jobId}`);
|
|
320
|
+
state.ledger.push({
|
|
321
|
+
id: newId('ledger'),
|
|
322
|
+
at: now,
|
|
323
|
+
type: 'STAKE',
|
|
324
|
+
agentId,
|
|
325
|
+
amount: -Math.abs(stakeAmount),
|
|
326
|
+
jobId,
|
|
327
|
+
reason: 'vote'
|
|
328
|
+
});
|
|
329
|
+
}
|
|
312
330
|
const targetType = input.targetType ?? (input.submissionId ? 'SUBMISSION' : input.choiceKey ? 'CHOICE' : undefined);
|
|
313
331
|
const targetId = input.targetId ?? input.submissionId;
|
|
314
332
|
if (targetType === 'SUBMISSION') {
|
|
@@ -330,7 +348,7 @@ export class JobEngine {
|
|
|
330
348
|
agentId,
|
|
331
349
|
score,
|
|
332
350
|
weight: input.weight ?? score,
|
|
333
|
-
stakeAmount:
|
|
351
|
+
stakeAmount: stakeAmount || undefined,
|
|
334
352
|
rationale: input.rationale,
|
|
335
353
|
createdAt: now
|
|
336
354
|
};
|
|
@@ -359,10 +377,28 @@ export class JobEngine {
|
|
|
359
377
|
if (arbiter && arbiter !== agentId) {
|
|
360
378
|
throw new Error('Only the trusted arbiter can resolve this job');
|
|
361
379
|
}
|
|
380
|
+
if (!input.manualWinners || input.manualWinners.length === 0) {
|
|
381
|
+
throw new Error('Trusted arbiter must provide manual winners to resolve');
|
|
382
|
+
}
|
|
362
383
|
}
|
|
363
384
|
|
|
364
|
-
if (job.consensusPolicy.type === 'OWNER_PICK'
|
|
365
|
-
|
|
385
|
+
if (job.consensusPolicy.type === 'OWNER_PICK') {
|
|
386
|
+
if (job.createdByAgentId !== agentId) {
|
|
387
|
+
throw new Error('Only the job creator can resolve this job');
|
|
388
|
+
}
|
|
389
|
+
if (!input.manualWinners || input.manualWinners.length === 0) {
|
|
390
|
+
throw new Error('Owner must provide manual winners to resolve');
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (job.consensusPolicy.type === 'APPROVAL_VOTE' && job.consensusPolicy.approvalVote?.settlement === 'oracle') {
|
|
395
|
+
const arbiter = job.consensusPolicy.trustedArbiterAgentId;
|
|
396
|
+
if (arbiter && arbiter !== agentId) {
|
|
397
|
+
throw new Error('Only the trusted arbiter can resolve this job');
|
|
398
|
+
}
|
|
399
|
+
if (!input.manualWinners || input.manualWinners.length === 0) {
|
|
400
|
+
throw new Error('Oracle settlement requires manual winners to resolve');
|
|
401
|
+
}
|
|
366
402
|
}
|
|
367
403
|
|
|
368
404
|
const submissions = state.submissions.filter((s) => s.jobId === jobId);
|
|
@@ -398,6 +434,41 @@ export class JobEngine {
|
|
|
398
434
|
}
|
|
399
435
|
}
|
|
400
436
|
|
|
437
|
+
// Vote-stake settlement for APPROVAL_VOTE (best-effort, local-first)
|
|
438
|
+
if (job.consensusPolicy.type === 'APPROVAL_VOTE' && job.consensusPolicy.approvalVote?.settlement === 'staked') {
|
|
439
|
+
const winnerSubmissionId = consensus.winningSubmissionIds?.[0];
|
|
440
|
+
const voteSlashPercent = Math.max(0, Math.min(1, job.consensusPolicy.approvalVote?.voteSlashPercent ?? 0));
|
|
441
|
+
for (const v of votes) {
|
|
442
|
+
const st = v.stakeAmount ?? 0;
|
|
443
|
+
if (!st || st <= 0) continue;
|
|
444
|
+
|
|
445
|
+
// Return stake by default
|
|
446
|
+
state.ledger.push({
|
|
447
|
+
id: newId('ledger'),
|
|
448
|
+
at: now,
|
|
449
|
+
type: 'UNSTAKE',
|
|
450
|
+
agentId: v.agentId,
|
|
451
|
+
amount: st,
|
|
452
|
+
jobId,
|
|
453
|
+
reason: 'vote'
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Slash if vote is "wrong" relative to winner.
|
|
457
|
+
// Wrong = YES on non-winner OR NO on winner.
|
|
458
|
+
const votedSubmissionId = v.submissionId ?? (v.targetType === 'SUBMISSION' ? v.targetId : undefined);
|
|
459
|
+
const isYes = (v.score ?? 0) > 0;
|
|
460
|
+
const isNo = (v.score ?? 0) < 0;
|
|
461
|
+
const wrong =
|
|
462
|
+
(winnerSubmissionId && votedSubmissionId && votedSubmissionId !== winnerSubmissionId && isYes) ||
|
|
463
|
+
(winnerSubmissionId && votedSubmissionId && votedSubmissionId === winnerSubmissionId && isNo);
|
|
464
|
+
|
|
465
|
+
if (wrong && voteSlashPercent > 0) {
|
|
466
|
+
const slashAmount = Math.min(st, st * voteSlashPercent);
|
|
467
|
+
slashes.push({ agentId: v.agentId, amount: slashAmount, reason: 'vote_wrong' });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
401
472
|
const submissionAgents = new Set(submissions.map((s) => s.agentId));
|
|
402
473
|
for (const bid of bids) {
|
|
403
474
|
const stakeAmount = bid.stakeAmount;
|
package/src/standalone.ts
CHANGED
|
@@ -163,7 +163,7 @@ function helpText() {
|
|
|
163
163
|
' jobs list [--tag <tag>] [--status <status>] [--mine] [--json]',
|
|
164
164
|
' submissions create <jobId> --artifact <json> [--summary <text>] [--confidence <n>] [--json]',
|
|
165
165
|
' submissions list <jobId> [--json]',
|
|
166
|
-
' votes cast <jobId> [--submission <id> | --choice <key>] [--weight <n>] [--json]',
|
|
166
|
+
' votes cast <jobId> [--submission <id> | --choice <key>] [--yes|--no] [--weight <n>] [--stake <n>] [--json]',
|
|
167
167
|
' votes list <jobId> [--json]',
|
|
168
168
|
' resolve <jobId> [--winner <agentId> ...] [--submission <submissionId>] [--json]',
|
|
169
169
|
' result get <jobId> [--json]',
|
|
@@ -54,8 +54,9 @@ export type RunnerResult = {
|
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
const POLICY_TYPES: ConsensusPolicyType[] = [
|
|
57
|
-
'
|
|
57
|
+
'FIRST_SUBMISSION_WINS',
|
|
58
58
|
'HIGHEST_CONFIDENCE_SINGLE',
|
|
59
|
+
'APPROVAL_VOTE',
|
|
59
60
|
'OWNER_PICK',
|
|
60
61
|
'TOP_K_SPLIT',
|
|
61
62
|
'MAJORITY_VOTE',
|
|
@@ -95,7 +96,13 @@ export async function runConsensusPolicyTests(options: RunnerOptions): Promise<R
|
|
|
95
96
|
trustedArbiterAgentId: policy === 'TRUSTED_ARBITER' ? 'arbiter' : '',
|
|
96
97
|
minConfidence: policy === 'HIGHEST_CONFIDENCE_SINGLE' ? 0.5 : undefined,
|
|
97
98
|
topK: policy === 'TOP_K_SPLIT' ? 2 : undefined,
|
|
98
|
-
ordering: policy === 'TOP_K_SPLIT' ? 'confidence' : undefined
|
|
99
|
+
ordering: policy === 'TOP_K_SPLIT' ? 'confidence' : undefined,
|
|
100
|
+
// APPROVAL_VOTE defaults (pure approval): require at least 1 vote and accept any positive score
|
|
101
|
+
quorum: policy === 'APPROVAL_VOTE' ? 1 : undefined,
|
|
102
|
+
minScore: policy === 'APPROVAL_VOTE' ? 1 : undefined,
|
|
103
|
+
minMargin: policy === 'APPROVAL_VOTE' ? 0 : undefined,
|
|
104
|
+
tieBreak: policy === 'APPROVAL_VOTE' ? 'earliest' : undefined,
|
|
105
|
+
approvalVote: policy === 'APPROVAL_VOTE' ? { weightMode: 'equal', settlement: 'immediate' } : undefined
|
|
99
106
|
},
|
|
100
107
|
slashingPolicy: { enabled: false, slashPercent: 0, slashFlat: 0 }
|
|
101
108
|
});
|
|
@@ -122,7 +129,7 @@ export async function runConsensusPolicyTests(options: RunnerOptions): Promise<R
|
|
|
122
129
|
const correctSubmission = submissions.find((sub) => sub.artifacts?.answer === script.expectedAnswer) || submissions[0];
|
|
123
130
|
log('[submission] selected correct', { submissionId: correctSubmission.id, agentId: correctSubmission.agentId });
|
|
124
131
|
|
|
125
|
-
if (policy === 'MAJORITY_VOTE' || policy === 'WEIGHTED_VOTE_SIMPLE' || policy === 'WEIGHTED_REPUTATION') {
|
|
132
|
+
if (policy === 'APPROVAL_VOTE' || policy === 'MAJORITY_VOTE' || policy === 'WEIGHTED_VOTE_SIMPLE' || policy === 'WEIGHTED_REPUTATION') {
|
|
126
133
|
for (const persona of personas) {
|
|
127
134
|
await engine.vote(persona.id, job.id, {
|
|
128
135
|
submissionId: correctSubmission.id,
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type ConsensusPolicyType =
|
|
2
|
-
| '
|
|
2
|
+
| 'FIRST_SUBMISSION_WINS'
|
|
3
3
|
| 'HIGHEST_CONFIDENCE_SINGLE'
|
|
4
|
+
| 'APPROVAL_VOTE'
|
|
4
5
|
| 'OWNER_PICK'
|
|
5
6
|
| 'TOP_K_SPLIT'
|
|
6
7
|
| 'MAJORITY_VOTE'
|
|
@@ -24,10 +25,27 @@ export type VoteTargetType = 'SUBMISSION' | 'CHOICE';
|
|
|
24
25
|
export interface ConsensusPolicyConfig {
|
|
25
26
|
type: ConsensusPolicyType;
|
|
26
27
|
trustedArbiterAgentId?: string;
|
|
28
|
+
|
|
29
|
+
// confidence-based policies
|
|
27
30
|
minConfidence?: number;
|
|
31
|
+
|
|
32
|
+
// top-k policies
|
|
28
33
|
topK?: number;
|
|
29
34
|
ordering?: 'confidence' | 'score';
|
|
35
|
+
|
|
36
|
+
// vote-based policies
|
|
30
37
|
quorum?: number;
|
|
38
|
+
minScore?: number;
|
|
39
|
+
minMargin?: number;
|
|
40
|
+
tieBreak?: 'earliest' | 'confidence' | 'arbiter';
|
|
41
|
+
|
|
42
|
+
// approval-vote specific
|
|
43
|
+
approvalVote?: {
|
|
44
|
+
weightMode?: 'equal' | 'explicit' | 'reputation';
|
|
45
|
+
settlement?: 'immediate' | 'staked' | 'oracle';
|
|
46
|
+
oracle?: 'trusted_arbiter';
|
|
47
|
+
voteSlashPercent?: number; // 0-1 (only for settlement=staked)
|
|
48
|
+
};
|
|
31
49
|
}
|
|
32
50
|
|
|
33
51
|
export interface SlashingPolicy {
|
|
@@ -162,7 +180,14 @@ export interface DiagnosticEntry {
|
|
|
162
180
|
context?: Record<string, unknown>;
|
|
163
181
|
}
|
|
164
182
|
|
|
165
|
-
export type LedgerEntryType =
|
|
183
|
+
export type LedgerEntryType =
|
|
184
|
+
| 'FAUCET'
|
|
185
|
+
| 'STAKE'
|
|
186
|
+
| 'UNSTAKE'
|
|
187
|
+
| 'PAYOUT'
|
|
188
|
+
| 'SLASH'
|
|
189
|
+
| 'ADJUST'
|
|
190
|
+
| 'ESCROW_MINT';
|
|
166
191
|
|
|
167
192
|
export interface LedgerEntry {
|
|
168
193
|
id: string;
|