M3: Mastra runtime, safety chain, two agents, Case Desk v1
- packages/domain: safety-chain objects (ValidationResult, SimulationResult,
EnvelopeMatch, StaleDenial, ExecutionReceipt, LineageRef/BidProposalDraft,
RouterDecision, BidExportFile) + fixtures on both sides.
- packages/services: proposal digest; PolicyEngine + hubei-spot-bidding pack
(digest-valid, bid-format, price-limits, quantity-non-negative,
ledger-consistency, lineage-integrity, originator-permission — each with
pass/fail tests); EnvelopeService; AuthorityService (fresh check, permits,
revoke, gateway validate); FileExportGateway (idempotent receipts);
Memory/File EventBus; LineageRecorder + P2 assembler; RevenueScenario
simulator; CaseDeskService; FsRepository; skill HTTP client moved here.
- packages/runtime: createRuntime (LibSQL storage, per-runtime workflow
factories), proposal-lifecycle (rule check → simulation → envelope gate
with suspend/resume → fresh check + permit → release), day-ahead-situation,
day-ahead-bid, TriggerService (scheduled/event/manual), LlmPort
(Mastra/Scripted/Null), Case Desk HTTP API, dev entry point.
- Tests: all eight docs/01 invariants, docs/07 06:00→08:30 end to end with
LLM down, restart survival of a suspended approval, permit expiry and
revocation, replay of a released proposal, trigger scheduling. 141 TS +
60 Python tests.
- Known gaps: ledger not yet persisted (replayed on restart); STALE ends the
run instead of looping to rule check; synthetic data stands in for
historical replay.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoYoGYzHkFyv3ALenkRPhA
2026-09-02 06:55:55 -04:00
|
|
|
|
import type { Approval, Curve96, Envelope, ForecastBundle, Proposal } from '@vpp/domain'
|
|
|
|
|
|
import { AuthorityService } from '../src/authority.js'
|
|
|
|
|
|
import { MemoryEventBus } from '../src/events.js'
|
|
|
|
|
|
import { LedgerService } from '../src/ledger.js'
|
|
|
|
|
|
import { LineageRecorder, assembleBidProposal, withLineage } from '../src/lineage.js'
|
|
|
|
|
|
import { HUBEI_BID_PACK_PLACEHOLDER, PolicyEngine, hubeiSpotBiddingPack } from '../src/policy.js'
|
|
|
|
|
|
import { MemorySnapshotStore } from '../src/snapshot.js'
|
|
|
|
|
|
|
|
|
|
|
|
export const NOW = '2026-03-14T08:00:00Z'
|
|
|
|
|
|
export const DATE = '2026-03-15'
|
|
|
|
|
|
export const PACK_VERSION = '2026.03'
|
|
|
|
|
|
|
|
|
|
|
|
export const curve = (v: string | string[], date = DATE): Curve96 => ({
|
|
|
|
|
|
interval_minutes: 15,
|
|
|
|
|
|
date,
|
|
|
|
|
|
values: Array.isArray(v) ? v : Array.from({ length: 96 }, () => v),
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
export const priceForecast = (p50 = '425.50'): ForecastBundle => ({
|
|
|
|
|
|
id: 'fc-price',
|
|
|
|
|
|
kind: 'PRICE',
|
|
|
|
|
|
market_date: DATE,
|
|
|
|
|
|
unit: 'yuan_per_mwh',
|
|
|
|
|
|
quantiles: { p10: curve('360.00'), p50: curve(p50), p90: curve('490.00') },
|
|
|
|
|
|
model: { name: 'price-forecast', version: '1.0.0' },
|
|
|
|
|
|
features_snapshot_ref: 'a'.repeat(64),
|
|
|
|
|
|
generated_at: NOW,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
/** A consistent little world: ledger with a monthly contract, a recorded MILP output, a proposal built from refs. */
|
|
|
|
|
|
export function world(opts: { qty?: string; price?: string; agent?: string } = {}) {
|
|
|
|
|
|
const snapshots = new MemorySnapshotStore()
|
|
|
|
|
|
const ledger = new LedgerService({ daMonthlyDeviationBand: '0.05', clock: () => NOW })
|
|
|
|
|
|
ledger.append({
|
|
|
|
|
|
id: 'contract-2026-03', timescale: 'MONTHLY', period: '2026-03', kind: 'CONTRACT',
|
|
|
|
|
|
energy_mwh: '37200.0', curve: null, source_ref: 'contract-2026-03-001', expected_version: 0,
|
|
|
|
|
|
}) // 1200 MWh/day → band [1140, 1260]
|
|
|
|
|
|
const engine = new PolicyEngine([hubeiSpotBiddingPack(PACK_VERSION, HUBEI_BID_PACK_PLACEHOLDER)])
|
|
|
|
|
|
const lineage = new LineageRecorder()
|
|
|
|
|
|
const events = new MemoryEventBus(() => NOW)
|
|
|
|
|
|
const authority = new AuthorityService(undefined, () => 'permit-001')
|
|
|
|
|
|
|
|
|
|
|
|
let n = 0
|
|
|
|
|
|
const milp = withLineage(lineage, snapshots, { tool: 'bid-optimization-milp', version: '1.0.0', idGen: () => `tc-${++n}` }, (_input: unknown) => ({
|
|
|
|
|
|
market_date: DATE,
|
|
|
|
|
|
prices_yuan_per_mwh: curve(opts.price ?? '0.00'),
|
|
|
|
|
|
quantities_mwh: curve(opts.qty ?? '12.5'), // 12.5 × 96 = 1200 MWh
|
|
|
|
|
|
expected_revenue_yuan: '510600.00',
|
|
|
|
|
|
}))
|
|
|
|
|
|
const forecastTool = withLineage(lineage, snapshots, { tool: 'price-forecast', version: '1.0.0', idGen: () => `tc-${++n}` }, (_i: unknown) => priceForecast())
|
|
|
|
|
|
|
M4: resource agent, envelopes live, review loop, insight cards
- packages/domain: AwardNotice, DISPATCH_PLAN proposal payload, ExecutionReport,
MeteringRecord, potential-assessment and dispatch-optimization contracts,
ReviewFinding (+ typed writebacks), SemanticMemoryEntry,
EnvelopeChangeRequest, InsightCard — exported with fixtures on both sides.
- skills-py: potential-assessment (certified × rolling fulfilment, evidence
days) and dispatch-optimization (per-interval LP on HiGHS, shortfall
reported) skills + routes + tests.
- packages/services: dispatch rules in the policy pack (over-allocation,
award anchor, lineage integrity for allocations); PowerBalanceSimulator;
SimulationGateway (permit-only, idempotent, seeded execute → ExecutionReports);
envelope deviation-streak suspension + apply(); ReviewService (attribution,
reliability EWMA writeback, semantic memory, envelope recommendations as
change requests); dispatch assembler; skill client methods.
- packages/runtime: resource agent; award-decomposition, review and
envelope-review workflows; lifecycle selects simulator/gateway by proposal
type; trigger hooks for awards, execution reports, metering; decide()
resumes either lifecycle or envelope-review runs; insight cards API.
- Tests: docs/07 D-1 16:00 and D+1 end to end; reliability score 0.9 → 0.880
and the next assessment de-rates capacity; envelope suspension on a seeded
3-day streak; WIDEN request applied only by a human. 184 TS + 80 Python.
- docs/open-questions: B10 (reliability/potential parameters). README and
CLAUDE.md status → M4 done, M5 next.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoYoGYzHkFyv3ALenkRPhA
2026-09-02 19:31:12 -04:00
|
|
|
|
const proposal: Proposal = lineage.run(() => {
|
M3: Mastra runtime, safety chain, two agents, Case Desk v1
- packages/domain: safety-chain objects (ValidationResult, SimulationResult,
EnvelopeMatch, StaleDenial, ExecutionReceipt, LineageRef/BidProposalDraft,
RouterDecision, BidExportFile) + fixtures on both sides.
- packages/services: proposal digest; PolicyEngine + hubei-spot-bidding pack
(digest-valid, bid-format, price-limits, quantity-non-negative,
ledger-consistency, lineage-integrity, originator-permission — each with
pass/fail tests); EnvelopeService; AuthorityService (fresh check, permits,
revoke, gateway validate); FileExportGateway (idempotent receipts);
Memory/File EventBus; LineageRecorder + P2 assembler; RevenueScenario
simulator; CaseDeskService; FsRepository; skill HTTP client moved here.
- packages/runtime: createRuntime (LibSQL storage, per-runtime workflow
factories), proposal-lifecycle (rule check → simulation → envelope gate
with suspend/resume → fresh check + permit → release), day-ahead-situation,
day-ahead-bid, TriggerService (scheduled/event/manual), LlmPort
(Mastra/Scripted/Null), Case Desk HTTP API, dev entry point.
- Tests: all eight docs/01 invariants, docs/07 06:00→08:30 end to end with
LLM down, restart survival of a suspended approval, permit expiry and
revocation, replay of a released proposal, trigger scheduling. 141 TS +
60 Python tests.
- Known gaps: ledger not yet persisted (replayed on restart); STALE ends the
run instead of looping to rule check; synthetic data stands in for
historical replay.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoYoGYzHkFyv3ALenkRPhA
2026-09-02 06:55:55 -04:00
|
|
|
|
forecastTool({ kind: 'PRICE' })
|
|
|
|
|
|
const { call } = milp({ market_date: DATE })
|
|
|
|
|
|
return assembleBidProposal(
|
|
|
|
|
|
{
|
|
|
|
|
|
market_date: DATE,
|
|
|
|
|
|
prices_ref: { tool_call_id: call.tool_call_id, path: 'prices_yuan_per_mwh' },
|
|
|
|
|
|
quantities_ref: { tool_call_id: call.tool_call_id, path: 'quantities_mwh' },
|
|
|
|
|
|
expected_revenue_ref: { tool_call_id: call.tool_call_id, path: 'expected_revenue_yuan' },
|
|
|
|
|
|
rationale: 'test',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'prop-001',
|
|
|
|
|
|
originator: { agent: opts.agent ?? 'trading-agent', trigger: 'SCHEDULED', task_id: 'task-001' },
|
|
|
|
|
|
lineage: lineage.current(),
|
|
|
|
|
|
ledger_version: ledger.read().version,
|
|
|
|
|
|
policy_pack_version: PACK_VERSION,
|
|
|
|
|
|
snapshots,
|
|
|
|
|
|
now: NOW,
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
const ruleCtx = () => ({ ledger: ledger.read(), ledgerService: ledger, snapshots, now: NOW })
|
|
|
|
|
|
return { snapshots, ledger, engine, lineage, events, authority, proposal, ruleCtx }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const envelope = (bounds: Record<string, string>, over: Partial<Envelope> = {}): Envelope => ({
|
|
|
|
|
|
id: 'env-bid-001',
|
|
|
|
|
|
scope: { proposal_type: 'BID', timescales: ['DAY_AHEAD'], resource_set: 'pool-hubei-01' },
|
|
|
|
|
|
bounds,
|
|
|
|
|
|
validity: { from: '2026-03-01T00:00:00Z', to: '2026-03-31T23:59:59Z' },
|
|
|
|
|
|
approval: { level: 'L1', approved_by: ['user-ops-lead'] },
|
|
|
|
|
|
escalation: { max_consecutive_deviations: 3, deviation_threshold_pct: '10.0' },
|
|
|
|
|
|
status: 'ACTIVE',
|
|
|
|
|
|
...over,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
export const approvalFor = (p: Proposal, ledgerVersion: number, over: Partial<Approval> = {}): Approval => ({
|
|
|
|
|
|
id: 'appr-001',
|
|
|
|
|
|
proposal_digest: p.digest,
|
|
|
|
|
|
decision: 'APPROVE',
|
|
|
|
|
|
approver: { id: 'user-trader-01', role: 'senior-trader' },
|
|
|
|
|
|
scope: { effect_type: 'BID', limits: {} },
|
|
|
|
|
|
validity: { from: NOW, to: '2026-03-15T09:00:00Z' },
|
|
|
|
|
|
evidence_versions: { policy_pack_version: PACK_VERSION, ledger_version: ledgerVersion, data_snapshot_refs: [] },
|
|
|
|
|
|
decided_at: NOW,
|
|
|
|
|
|
...over,
|
|
|
|
|
|
})
|