@pi-in-go/pigpen-pi-typesafe 0.1.0

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.
Files changed (189) hide show
  1. package/CREDITS.md +14 -0
  2. package/LICENSE +22 -0
  3. package/README.md +45 -0
  4. package/extensions/pi-typesafe/branches_test.go +185 -0
  5. package/extensions/pi-typesafe/command.go +319 -0
  6. package/extensions/pi-typesafe/export_test.go +9 -0
  7. package/extensions/pi-typesafe/extension.go +188 -0
  8. package/extensions/pi-typesafe/extension_test.go +321 -0
  9. package/extensions/pi-typesafe/fakehost_test.go +548 -0
  10. package/extensions/pi-typesafe/format.go +191 -0
  11. package/extensions/pi-typesafe/format_test.go +75 -0
  12. package/extensions/pi-typesafe/go.mod +10 -0
  13. package/extensions/pi-typesafe/go.sum +2 -0
  14. package/extensions/pi-typesafe/go.work +11 -0
  15. package/extensions/pi-typesafe/harness_test.go +200 -0
  16. package/extensions/pi-typesafe/ownmodel_test.go +100 -0
  17. package/extensions/pi-typesafe/review_test.go +134 -0
  18. package/extensions/pi-typesafe/tool.go +193 -0
  19. package/extensions/pi-typesafe/twin_test.go +28 -0
  20. package/libs/pi-typesafe-api/CREDITS.md +14 -0
  21. package/libs/pi-typesafe-api/LICENSE +22 -0
  22. package/libs/pi-typesafe-api/README.md +30 -0
  23. package/libs/pi-typesafe-api/ask.go +62 -0
  24. package/libs/pi-typesafe-api/ask_test.go +76 -0
  25. package/libs/pi-typesafe-api/auth.go +249 -0
  26. package/libs/pi-typesafe-api/auth_test.go +131 -0
  27. package/libs/pi-typesafe-api/backends.go +336 -0
  28. package/libs/pi-typesafe-api/backends_test.go +404 -0
  29. package/libs/pi-typesafe-api/batch.go +202 -0
  30. package/libs/pi-typesafe-api/batch_test.go +202 -0
  31. package/libs/pi-typesafe-api/battery_test.go +41 -0
  32. package/libs/pi-typesafe-api/calibrate.go +354 -0
  33. package/libs/pi-typesafe-api/calibrate_test.go +186 -0
  34. package/libs/pi-typesafe-api/client.go +615 -0
  35. package/libs/pi-typesafe-api/client_test.go +490 -0
  36. package/libs/pi-typesafe-api/credentials.go +252 -0
  37. package/libs/pi-typesafe-api/credentials_test.go +216 -0
  38. package/libs/pi-typesafe-api/doc.go +14 -0
  39. package/libs/pi-typesafe-api/errors.go +143 -0
  40. package/libs/pi-typesafe-api/evaluation.go +86 -0
  41. package/libs/pi-typesafe-api/evaluation_schema.json +264 -0
  42. package/libs/pi-typesafe-api/gaps_test.go +77 -0
  43. package/libs/pi-typesafe-api/go.mod +9 -0
  44. package/libs/pi-typesafe-api/go.sum +2 -0
  45. package/libs/pi-typesafe-api/helpers_test.go +169 -0
  46. package/libs/pi-typesafe-api/hostmodel/hostmodel.go +87 -0
  47. package/libs/pi-typesafe-api/json.go +299 -0
  48. package/libs/pi-typesafe-api/json_test.go +92 -0
  49. package/libs/pi-typesafe-api/ownmodel_test.go +79 -0
  50. package/libs/pi-typesafe-api/package.json +40 -0
  51. package/libs/pi-typesafe-api/provenance.json +18 -0
  52. package/libs/pi-typesafe-api/review_test.go +23 -0
  53. package/libs/pi-typesafe-api/schema.go +473 -0
  54. package/libs/pi-typesafe-api/schema_test.go +262 -0
  55. package/libs/pi-typesafe-api/testdata/tools/typebox-messages.mts +5 -0
  56. package/libs/pi-typesafe-api/testdata/typebox-messages.json +285 -0
  57. package/libs/pi-typesafe-api/twin_test.go +28 -0
  58. package/libs/pi-typesafe-api/ui/fakehost_test.go +548 -0
  59. package/libs/pi-typesafe-api/ui/keyprompt.go +115 -0
  60. package/libs/pi-typesafe-api/ui/login.go +106 -0
  61. package/libs/pi-typesafe-api/ui/twin_test.go +28 -0
  62. package/libs/pi-typesafe-api/ui/ui_test.go +285 -0
  63. package/libs/pi-typesafe-api/usage.go +366 -0
  64. package/libs/pi-typesafe-api/usage_test.go +139 -0
  65. package/libs/typesafe/CONTRACT.md +125 -0
  66. package/libs/typesafe/CREDITS.md +37 -0
  67. package/libs/typesafe/LICENSE +23 -0
  68. package/libs/typesafe/README.md +19 -0
  69. package/libs/typesafe/go.mod +3 -0
  70. package/libs/typesafe/libraries/ownmodel/backend_test.go +496 -0
  71. package/libs/typesafe/libraries/ownmodel/canon.go +190 -0
  72. package/libs/typesafe/libraries/ownmodel/convert.go +199 -0
  73. package/libs/typesafe/libraries/ownmodel/doc.go +15 -0
  74. package/libs/typesafe/libraries/ownmodel/equivalence_test.go +199 -0
  75. package/libs/typesafe/libraries/ownmodel/helpers_test.go +155 -0
  76. package/libs/typesafe/libraries/ownmodel/mutation_test.go +31 -0
  77. package/libs/typesafe/libraries/ownmodel/ownmodel.go +225 -0
  78. package/libs/typesafe/libraries/ownmodel/plan.go +442 -0
  79. package/libs/typesafe/libraries/ownmodel/run.go +288 -0
  80. package/libs/typesafe/libraries/ownmodel/schema_test.go +254 -0
  81. package/libs/typesafe/libraries/ownmodel/twins_test.go +169 -0
  82. package/libs/typesafe/libraries/ownmodel/utils_test.go +125 -0
  83. package/libs/typesafe/libraries/pigmodel/pigmodel.go +264 -0
  84. package/libs/typesafe/libraries/pigmodel/pigmodel_test.go +410 -0
  85. package/libs/typesafe/libraries/typesafe/answers.go +268 -0
  86. package/libs/typesafe/libraries/typesafe/api_response_test.go +113 -0
  87. package/libs/typesafe/libraries/typesafe/batch.go +80 -0
  88. package/libs/typesafe/libraries/typesafe/batch_test.go +133 -0
  89. package/libs/typesafe/libraries/typesafe/bench_test.go +71 -0
  90. package/libs/typesafe/libraries/typesafe/client.go +561 -0
  91. package/libs/typesafe/libraries/typesafe/client_test.go +495 -0
  92. package/libs/typesafe/libraries/typesafe/crosscheck_test.go +464 -0
  93. package/libs/typesafe/libraries/typesafe/crosscheck_workflowevals_test.go +219 -0
  94. package/libs/typesafe/libraries/typesafe/doc.go +27 -0
  95. package/libs/typesafe/libraries/typesafe/entry.go +142 -0
  96. package/libs/typesafe/libraries/typesafe/env.go +11 -0
  97. package/libs/typesafe/libraries/typesafe/errors.go +310 -0
  98. package/libs/typesafe/libraries/typesafe/errors_test.go +175 -0
  99. package/libs/typesafe/libraries/typesafe/helpers_test.go +294 -0
  100. package/libs/typesafe/libraries/typesafe/live_test.go +96 -0
  101. package/libs/typesafe/libraries/typesafe/logging.go +160 -0
  102. package/libs/typesafe/libraries/typesafe/logging_test.go +259 -0
  103. package/libs/typesafe/libraries/typesafe/marshal_test.go +112 -0
  104. package/libs/typesafe/libraries/typesafe/mutation_test.go +39 -0
  105. package/libs/typesafe/libraries/typesafe/questions.go +490 -0
  106. package/libs/typesafe/libraries/typesafe/questions_test.go +166 -0
  107. package/libs/typesafe/libraries/typesafe/regressions_test.go +159 -0
  108. package/libs/typesafe/libraries/typesafe/reliability_test.go +649 -0
  109. package/libs/typesafe/libraries/typesafe/retry.go +350 -0
  110. package/libs/typesafe/libraries/typesafe/retry_test.go +297 -0
  111. package/libs/typesafe/libraries/typesafe/runtime_test.go +26 -0
  112. package/libs/typesafe/libraries/typesafe/transport_test.go +163 -0
  113. package/libs/typesafe/libraries/typesafe/twins_test.go +127 -0
  114. package/libs/typesafe/libraries/typesafe/types_test.go +165 -0
  115. package/libs/typesafe/libraries/typesafe/version.go +10 -0
  116. package/libs/typesafe/package.json +37 -0
  117. package/libs/typesafe/provenance.json +49 -0
  118. package/package.json +42 -0
  119. package/port/PORT.md +98 -0
  120. package/port/accepted-gaps.json +3 -0
  121. package/port/golden/enable-confirm.jsonl +11 -0
  122. package/port/golden/enable-decline.jsonl +20 -0
  123. package/port/golden/enable-missing-key.jsonl +4 -0
  124. package/port/golden/login-shadow.jsonl +4 -0
  125. package/port/golden/logout-env-key.jsonl +6 -0
  126. package/port/golden/playground-cancel.jsonl +4 -0
  127. package/port/golden/playground-invalid-json.jsonl +5 -0
  128. package/port/golden/playground-invalid-questions.jsonl +5 -0
  129. package/port/golden/status-env-key.jsonl +6 -0
  130. package/port/golden/status-no-key.jsonl +6 -0
  131. package/port/golden/tool-disabled.jsonl +18 -0
  132. package/port/golden/trailing-words.jsonl +10 -0
  133. package/port/library-mutations.py +44 -0
  134. package/port/mutations.json +302 -0
  135. package/port/oracle/.env.example +4 -0
  136. package/port/oracle/CHANGELOG.md +91 -0
  137. package/port/oracle/CONTRIBUTING.md +35 -0
  138. package/port/oracle/LICENSE +21 -0
  139. package/port/oracle/README.md +159 -0
  140. package/port/oracle/docs/api.md +143 -0
  141. package/port/oracle/docs/ci-cd.md +97 -0
  142. package/port/oracle/examples/decision-extension.ts +41 -0
  143. package/port/oracle/extensions/index.js +2 -0
  144. package/port/oracle/package.json +89 -0
  145. package/port/oracle/scripts/dev-pi.mjs +23 -0
  146. package/port/oracle/scripts/live-smoke.mjs +35 -0
  147. package/port/oracle/src/ask.ts +42 -0
  148. package/port/oracle/src/auth.ts +171 -0
  149. package/port/oracle/src/backends.ts +196 -0
  150. package/port/oracle/src/batch.ts +170 -0
  151. package/port/oracle/src/calibrate.ts +237 -0
  152. package/port/oracle/src/client.ts +310 -0
  153. package/port/oracle/src/credentials.ts +136 -0
  154. package/port/oracle/src/errors.ts +53 -0
  155. package/port/oracle/src/extension.ts +204 -0
  156. package/port/oracle/src/index.ts +31 -0
  157. package/port/oracle/src/key-prompt.ts +51 -0
  158. package/port/oracle/src/login.ts +60 -0
  159. package/port/oracle/src/schema.ts +158 -0
  160. package/port/oracle/src/ui.ts +4 -0
  161. package/port/oracle/src/usage.ts +258 -0
  162. package/port/oracle/tests/ask.test.ts +63 -0
  163. package/port/oracle/tests/auth.test.ts +141 -0
  164. package/port/oracle/tests/backends.test.ts +380 -0
  165. package/port/oracle/tests/batch.test.ts +156 -0
  166. package/port/oracle/tests/calibrate.test.ts +144 -0
  167. package/port/oracle/tests/client.test.ts +499 -0
  168. package/port/oracle/tests/credentials.test.ts +144 -0
  169. package/port/oracle/tests/extension.test.ts +276 -0
  170. package/port/oracle/tests/key-prompt.test.ts +47 -0
  171. package/port/oracle/tests/login.test.ts +101 -0
  172. package/port/oracle/tests/schema.test.ts +85 -0
  173. package/port/oracle/tests/usage.test.ts +106 -0
  174. package/port/oracle/tsconfig.build.json +10 -0
  175. package/port/oracle/tsconfig.json +14 -0
  176. package/port/scenarios/enable-confirm.json +5 -0
  177. package/port/scenarios/enable-decline.json +3 -0
  178. package/port/scenarios/enable-missing-key.json +2 -0
  179. package/port/scenarios/login-shadow.json +2 -0
  180. package/port/scenarios/logout-env-key.json +3 -0
  181. package/port/scenarios/playground-cancel.json +2 -0
  182. package/port/scenarios/playground-invalid-json.json +2 -0
  183. package/port/scenarios/playground-invalid-questions.json +2 -0
  184. package/port/scenarios/status-env-key.json +3 -0
  185. package/port/scenarios/status-no-key.json +3 -0
  186. package/port/scenarios/tool-disabled.json +2 -0
  187. package/port/scenarios/trailing-words.json +5 -0
  188. package/port/upstream-tests.json +160 -0
  189. package/provenance.json +18 -0
@@ -0,0 +1,366 @@
1
+ package pitypesafe
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "math"
7
+ "os"
8
+ "path/filepath"
9
+ "regexp"
10
+ "sort"
11
+ "strconv"
12
+ "strings"
13
+ "sync"
14
+ "time"
15
+ )
16
+
17
+ // DefaultUSDPerMTok mirrors the $42-per-billion-input-token rate the original's README quotes, so a spend cap
18
+ // means something before anyone configures a price. TypeSafe bills input tokens only; output is free.
19
+ const DefaultUSDPerMTok = 0.042
20
+
21
+ const (
22
+ keepDays = 31
23
+ usageVersion = 1
24
+ )
25
+
26
+ // UsageTotals are counters for one window (a session, or one local day).
27
+ type UsageTotals struct {
28
+ RequestsStarted int `json:"requestsStarted"`
29
+ RequestsSucceeded int `json:"requestsSucceeded"`
30
+ RequestsFailed int `json:"requestsFailed"`
31
+ InputTokens int `json:"inputTokens"`
32
+ OutputTokens int `json:"outputTokens"`
33
+ }
34
+
35
+ // UsageReport is one day of persisted totals plus the cost estimate for that day.
36
+ type UsageReport struct {
37
+ UsageTotals
38
+ Day string
39
+ EstimatedUSD float64
40
+ }
41
+
42
+ // SpendCaps are client-side spend caps. A zero field is unlimited. A cap that is reached raises a budget
43
+ // error before the next request leaves the process.
44
+ type SpendCaps struct {
45
+ MaxRequests int
46
+ MaxRequestsPerDay int
47
+ MaxInputTokensPerDay int
48
+ MaxUSDPerDay float64
49
+ }
50
+
51
+ // BlockedCapName names a reached cap.
52
+ type BlockedCapName string
53
+
54
+ // The day caps.
55
+ const (
56
+ CapRequestsPerDay BlockedCapName = "requestsPerDay"
57
+ CapInputTokensPerDay BlockedCapName = "inputTokensPerDay"
58
+ CapUSDPerDay BlockedCapName = "usdPerDay"
59
+ )
60
+
61
+ // BlockedCap is the cap that stops the next request, with what it allows and what has been used today.
62
+ type BlockedCap struct {
63
+ Cap BlockedCapName
64
+ Limit float64
65
+ Used float64
66
+ Day string
67
+ }
68
+
69
+ var dayPattern = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
70
+
71
+ // LocalDay is the local calendar day of t as YYYY-MM-DD.
72
+ func LocalDay(t time.Time) string { return t.Format("2006-01-02") }
73
+
74
+ // UsagePath is the stored ledger, alongside the key store so one directory holds every file of this package.
75
+ func UsagePath() string { return filepath.Join(TypeSafeDir(), "usage.json") }
76
+
77
+ // EstimateUSD is the input-token cost, rounded to a micro-dollar so the number stays readable.
78
+ func EstimateUSD(inputTokens int, usdPerMTok float64) float64 {
79
+ return math.Floor(float64(inputTokens)*usdPerMTok/1e6*1e6+0.5) / 1e6
80
+ }
81
+
82
+ // EmptyTotals returns zeroed counters.
83
+ func EmptyTotals() UsageTotals { return UsageTotals{} }
84
+
85
+ // CapsFromEnvironment reads the caps a headless run may set without code: PI_TYPESAFE_MAX_USD_PER_DAY and its
86
+ // siblings. getenv nil uses the process environment. Values that are not positive numbers are ignored.
87
+ func CapsFromEnvironment(getenv func(string) string) SpendCaps {
88
+ if getenv == nil {
89
+ getenv = os.Getenv
90
+ }
91
+ number := func(name string) float64 {
92
+ raw := strings.TrimSpace(getenv(name))
93
+ if raw == "" {
94
+ return 0
95
+ }
96
+ v, err := strconv.ParseFloat(raw, 64)
97
+ if err != nil || math.IsNaN(v) || math.IsInf(v, 0) || v <= 0 {
98
+ return 0
99
+ }
100
+ return v
101
+ }
102
+ return SpendCaps{
103
+ MaxRequestsPerDay: int(math.Floor(number("PI_TYPESAFE_MAX_REQUESTS_PER_DAY"))),
104
+ MaxInputTokensPerDay: int(math.Floor(number("PI_TYPESAFE_MAX_INPUT_TOKENS_PER_DAY"))),
105
+ MaxUSDPerDay: number("PI_TYPESAFE_MAX_USD_PER_DAY"),
106
+ }
107
+ }
108
+
109
+ // MergeCaps combines an explicit cap with the environment's; the lowest set value wins, so the environment may
110
+ // lower an explicit cap and never raise it. The session cap comes from the explicit caps only.
111
+ func MergeCaps(explicit, environment SpendCaps) SpendCaps {
112
+ lowestInt := func(a, b int) int {
113
+ switch {
114
+ case a == 0:
115
+ return b
116
+ case b == 0:
117
+ return a
118
+ }
119
+ return min(a, b)
120
+ }
121
+ lowestFloat := func(a, b float64) float64 {
122
+ switch {
123
+ case a == 0:
124
+ return b
125
+ case b == 0:
126
+ return a
127
+ }
128
+ return math.Min(a, b)
129
+ }
130
+ return SpendCaps{
131
+ MaxRequests: explicit.MaxRequests,
132
+ MaxRequestsPerDay: lowestInt(explicit.MaxRequestsPerDay, environment.MaxRequestsPerDay),
133
+ MaxInputTokensPerDay: lowestInt(explicit.MaxInputTokensPerDay, environment.MaxInputTokensPerDay),
134
+ MaxUSDPerDay: lowestFloat(explicit.MaxUSDPerDay, environment.MaxUSDPerDay),
135
+ }
136
+ }
137
+
138
+ // LedgerOptions configure OpenUsageLedger.
139
+ type LedgerOptions struct {
140
+ // Path defaults to UsagePath(); tests point it at a temporary file.
141
+ Path string
142
+ // Now is the clock for the local day and for rollover; injectable for tests.
143
+ Now func() time.Time
144
+ UsdPerMTok float64
145
+ }
146
+
147
+ // UsageLedger is one day of persisted request, token, and cost totals, plus the caps that stop the next
148
+ // request. The in-memory copy is authoritative for this process; the file is the cross-process,
149
+ // across-restart record. Reads are defensive, writes are atomic and best-effort, and a day rolls over on the
150
+ // local date, so a long eval cannot accumulate forever unnoticed. It is safe for concurrent use.
151
+ type UsageLedger interface {
152
+ Path() string
153
+ UsdPerMTok() float64
154
+ // Today returns today's totals, after any rollover.
155
+ Today() UsageReport
156
+ // RecordStart counts the attempt before it is submitted; a request that never returns still counts.
157
+ RecordStart()
158
+ RecordSuccess(inputTokens, outputTokens int)
159
+ RecordFailure()
160
+ // Blocked returns the reached day cap that blocks the next request, or nil. The caller owns the caps.
161
+ Blocked(caps SpendCaps) *BlockedCap
162
+ // Describe is one line for status output: today's requests, tokens, and cost, with the caps that apply.
163
+ Describe(caps SpendCaps) string
164
+ }
165
+
166
+ type fileLedger struct {
167
+ mu sync.Mutex
168
+ path string
169
+ now func() time.Time
170
+ usdPerMTok float64
171
+ day string
172
+ days map[string]UsageTotals
173
+ totals UsageTotals
174
+ }
175
+
176
+ // OpenUsageLedger opens (or starts) the ledger file.
177
+ func OpenUsageLedger(opts LedgerOptions) UsageLedger {
178
+ l := &fileLedger{path: opts.Path, now: opts.Now, usdPerMTok: opts.UsdPerMTok}
179
+ if l.path == "" {
180
+ l.path = UsagePath()
181
+ }
182
+ if l.now == nil {
183
+ l.now = time.Now
184
+ }
185
+ if l.usdPerMTok <= 0 {
186
+ l.usdPerMTok = DefaultUSDPerMTok
187
+ }
188
+ l.day = LocalDay(l.now())
189
+ l.days = keepRecent(readDays(l.path), l.day)
190
+ l.totals = l.days[l.day]
191
+ return l
192
+ }
193
+
194
+ func count(v any) int {
195
+ f, ok := v.(float64)
196
+ if !ok || f < 0 || f != math.Floor(f) || f > 1<<53-1 {
197
+ return 0
198
+ }
199
+ return int(f)
200
+ }
201
+
202
+ func totalsOf(v any) UsageTotals {
203
+ raw, _ := v.(map[string]any)
204
+ return UsageTotals{
205
+ RequestsStarted: count(raw["requestsStarted"]),
206
+ RequestsSucceeded: count(raw["requestsSucceeded"]),
207
+ RequestsFailed: count(raw["requestsFailed"]),
208
+ InputTokens: count(raw["inputTokens"]),
209
+ OutputTokens: count(raw["outputTokens"]),
210
+ }
211
+ }
212
+
213
+ // readDays is defensive: a missing, unreadable, or corrupt ledger restarts today's count; it never blocks a request.
214
+ func readDays(path string) map[string]UsageTotals {
215
+ out := map[string]UsageTotals{}
216
+ data, err := os.ReadFile(path)
217
+ if err != nil {
218
+ return out
219
+ }
220
+ var parsed map[string]any
221
+ if json.Unmarshal(data, &parsed) != nil {
222
+ return out
223
+ }
224
+ days, ok := parsed["days"].(map[string]any)
225
+ if !ok {
226
+ return out
227
+ }
228
+ for day, totals := range days {
229
+ if dayPattern.MatchString(day) {
230
+ out[day] = totalsOf(totals)
231
+ }
232
+ }
233
+ return out
234
+ }
235
+
236
+ func keepRecent(days map[string]UsageTotals, today string) map[string]UsageTotals {
237
+ names := make([]string, 0, len(days))
238
+ for n := range days {
239
+ names = append(names, n)
240
+ }
241
+ sort.Strings(names)
242
+ if len(names) > keepDays {
243
+ names = names[len(names)-keepDays:]
244
+ }
245
+ out := map[string]UsageTotals{}
246
+ for _, n := range names {
247
+ out[n] = days[n]
248
+ }
249
+ if _, ok := days[today]; ok {
250
+ out[today] = days[today]
251
+ } else {
252
+ out[today] = UsageTotals{}
253
+ }
254
+ return out
255
+ }
256
+
257
+ // writeDays is owner-only, atomic, and best-effort: a ledger this process cannot write never fails a request.
258
+ func writeDays(path string, days map[string]UsageTotals) {
259
+ body, err := json.MarshalIndent(map[string]any{"version": usageVersion, "days": days}, "", " ")
260
+ if err != nil {
261
+ return
262
+ }
263
+ _ = writeOwnerOnly(path, append(body, '\n'))
264
+ }
265
+
266
+ func (l *fileLedger) Path() string { return l.path }
267
+ func (l *fileLedger) UsdPerMTok() float64 { return l.usdPerMTok }
268
+
269
+ func (l *fileLedger) report(t UsageTotals, day string) UsageReport {
270
+ return UsageReport{UsageTotals: t, Day: day, EstimatedUSD: EstimateUSD(t.InputTokens, l.usdPerMTok)}
271
+ }
272
+
273
+ func (l *fileLedger) save() {
274
+ l.days = keepRecent(mergeDay(l.days, l.day, l.totals), l.day)
275
+ writeDays(l.path, l.days)
276
+ }
277
+
278
+ func mergeDay(days map[string]UsageTotals, day string, totals UsageTotals) map[string]UsageTotals {
279
+ out := make(map[string]UsageTotals, len(days)+1)
280
+ for k, v := range days {
281
+ out[k] = v
282
+ }
283
+ out[day] = totals
284
+ return out
285
+ }
286
+
287
+ func (l *fileLedger) roll() {
288
+ current := LocalDay(l.now())
289
+ if current == l.day {
290
+ return
291
+ }
292
+ l.day = current
293
+ l.totals = l.days[l.day]
294
+ l.days = keepRecent(l.days, l.day)
295
+ }
296
+
297
+ func (l *fileLedger) add(d UsageTotals) {
298
+ l.mu.Lock()
299
+ defer l.mu.Unlock()
300
+ l.roll()
301
+ l.totals = UsageTotals{
302
+ RequestsStarted: l.totals.RequestsStarted + d.RequestsStarted,
303
+ RequestsSucceeded: l.totals.RequestsSucceeded + d.RequestsSucceeded,
304
+ RequestsFailed: l.totals.RequestsFailed + d.RequestsFailed,
305
+ InputTokens: l.totals.InputTokens + d.InputTokens,
306
+ OutputTokens: l.totals.OutputTokens + d.OutputTokens,
307
+ }
308
+ l.save()
309
+ }
310
+
311
+ func (l *fileLedger) Today() UsageReport {
312
+ l.mu.Lock()
313
+ defer l.mu.Unlock()
314
+ l.roll()
315
+ return l.report(l.totals, l.day)
316
+ }
317
+
318
+ func (l *fileLedger) RecordStart() { l.add(UsageTotals{RequestsStarted: 1}) }
319
+ func (l *fileLedger) RecordSuccess(in, out int) {
320
+ l.add(UsageTotals{RequestsSucceeded: 1, InputTokens: max(in, 0), OutputTokens: max(out, 0)})
321
+ }
322
+ func (l *fileLedger) RecordFailure() { l.add(UsageTotals{RequestsFailed: 1}) }
323
+
324
+ func (l *fileLedger) Blocked(caps SpendCaps) *BlockedCap {
325
+ l.mu.Lock()
326
+ defer l.mu.Unlock()
327
+ l.roll()
328
+ checks := []struct {
329
+ cap BlockedCapName
330
+ limit float64
331
+ used float64
332
+ }{
333
+ {CapRequestsPerDay, float64(caps.MaxRequestsPerDay), float64(l.totals.RequestsStarted)},
334
+ {CapInputTokensPerDay, float64(caps.MaxInputTokensPerDay), float64(l.totals.InputTokens)},
335
+ {CapUSDPerDay, caps.MaxUSDPerDay, EstimateUSD(l.totals.InputTokens, l.usdPerMTok)},
336
+ }
337
+ for _, c := range checks {
338
+ if c.limit > 0 && c.used >= c.limit {
339
+ return &BlockedCap{Cap: c.cap, Limit: c.limit, Used: c.used, Day: l.day}
340
+ }
341
+ }
342
+ return nil
343
+ }
344
+
345
+ func (l *fileLedger) Describe(caps SpendCaps) string {
346
+ l.mu.Lock()
347
+ defer l.mu.Unlock()
348
+ l.roll()
349
+ cur := l.report(l.totals, l.day)
350
+ var limits []string
351
+ if caps.MaxRequestsPerDay > 0 {
352
+ limits = append(limits, fmt.Sprintf("%d/%d requests", cur.RequestsStarted, caps.MaxRequestsPerDay))
353
+ }
354
+ if caps.MaxInputTokensPerDay > 0 {
355
+ limits = append(limits, fmt.Sprintf("%d/%d input tokens", cur.InputTokens, caps.MaxInputTokensPerDay))
356
+ }
357
+ if caps.MaxUSDPerDay > 0 {
358
+ limits = append(limits, fmt.Sprintf("$%.4f/$%.2f", cur.EstimatedUSD, caps.MaxUSDPerDay))
359
+ }
360
+ tail := "; no daily cap"
361
+ if len(limits) > 0 {
362
+ tail = "; caps " + strings.Join(limits, ", ")
363
+ }
364
+ return fmt.Sprintf("%d requests today (%d ok, %d failed), %d input / %d output tokens, ~$%.4f%s",
365
+ cur.RequestsStarted, cur.RequestsSucceeded, cur.RequestsFailed, cur.InputTokens, cur.OutputTokens, cur.EstimatedUSD, tail)
366
+ }
@@ -0,0 +1,139 @@
1
+ package pitypesafe
2
+
3
+ import (
4
+ "encoding/json"
5
+ "os"
6
+ "path/filepath"
7
+ "testing"
8
+ "time"
9
+ )
10
+
11
+ func at(day, hour int) time.Time {
12
+ return time.Date(2026, time.January, day, hour, 0, 0, 0, time.Local)
13
+ }
14
+
15
+ func TestUsage(t *testing.T) {
16
+ tw(t, "usage", "a ledger counts requests, tokens, and failures, and prices input tokens only", func(t *testing.T) {
17
+ l := OpenUsageLedger(LedgerOptions{Path: filepath.Join(t.TempDir(), "counts.json"), Now: func() time.Time { return at(1, 12) }})
18
+ if got := l.Today(); got != (UsageReport{Day: "2026-01-01"}) {
19
+ t.Fatalf("empty ledger = %+v", got)
20
+ }
21
+ l.RecordStart()
22
+ l.RecordSuccess(42, 7)
23
+ l.RecordStart()
24
+ l.RecordFailure()
25
+ today := l.Today()
26
+ if today.RequestsStarted != 2 || today.RequestsSucceeded != 1 || today.RequestsFailed != 1 || today.InputTokens != 42 || today.OutputTokens != 7 || today.Day != "2026-01-01" {
27
+ t.Fatalf("totals = %+v", today)
28
+ }
29
+ // Output tokens are free; only input tokens carry a price.
30
+ if today.EstimatedUSD != EstimateUSD(42, DefaultUSDPerMTok) {
31
+ t.Errorf("estimate = %v", today.EstimatedUSD)
32
+ }
33
+ d := l.Describe(SpendCaps{})
34
+ if !contains(d, "2 requests today (1 ok, 1 failed)") || !contains(d, "~$0.0000") {
35
+ t.Errorf("describe = %q", d)
36
+ }
37
+ })
38
+ tw(t, "usage", "totals survive a new ledger instance, so a restart does not reset the day", func(t *testing.T) {
39
+ path := filepath.Join(t.TempDir(), "persist.json")
40
+ now := func() time.Time { return at(2, 12) }
41
+ first := OpenUsageLedger(LedgerOptions{Path: path, Now: now})
42
+ first.RecordStart()
43
+ first.RecordSuccess(1000, 0)
44
+ second := OpenUsageLedger(LedgerOptions{Path: path, Now: now})
45
+ if got := second.Today(); got.RequestsStarted != 1 || got.InputTokens != 1000 || got.EstimatedUSD != EstimateUSD(1000, DefaultUSDPerMTok) {
46
+ t.Fatalf("reopened = %+v", got)
47
+ }
48
+ info, err := os.Stat(path)
49
+ if err != nil || info.Mode().Perm() != 0o600 {
50
+ t.Fatalf("mode = %v, %v", info, err)
51
+ }
52
+ })
53
+ tw(t, "usage", "the day rolls over on the local date and old days are kept", func(t *testing.T) {
54
+ day := 3
55
+ path := filepath.Join(t.TempDir(), "rollover.json")
56
+ l := OpenUsageLedger(LedgerOptions{Path: path, Now: func() time.Time { return at(day, 12) }})
57
+ l.RecordStart()
58
+ l.RecordSuccess(500, 0)
59
+ day = 4
60
+ if got := l.Today(); got.Day != "2026-01-04" || got.RequestsStarted != 0 || got.InputTokens != 0 {
61
+ t.Fatalf("after rollover = %+v", got)
62
+ }
63
+ l.RecordStart()
64
+ var file struct {
65
+ Days map[string]UsageTotals `json:"days"`
66
+ }
67
+ data, _ := os.ReadFile(path)
68
+ if err := json.Unmarshal(data, &file); err != nil {
69
+ t.Fatal(err)
70
+ }
71
+ if file.Days["2026-01-03"].InputTokens != 500 || file.Days["2026-01-04"].RequestsStarted != 1 {
72
+ t.Fatalf("file = %s", data)
73
+ }
74
+ })
75
+ tw(t, "usage", "each cap stops the next request and names itself", func(t *testing.T) {
76
+ dir := t.TempDir()
77
+ now := func() time.Time { return at(5, 12) }
78
+ counting := OpenUsageLedger(LedgerOptions{Path: filepath.Join(dir, "r.json"), Now: now})
79
+ requests := SpendCaps{MaxRequestsPerDay: 1}
80
+ if counting.Blocked(requests) != nil {
81
+ t.Fatal("blocked before any request")
82
+ }
83
+ counting.RecordStart()
84
+ if got := counting.Blocked(requests); got == nil || *got != (BlockedCap{Cap: CapRequestsPerDay, Limit: 1, Used: 1, Day: "2026-01-05"}) {
85
+ t.Fatalf("blocked = %+v", got)
86
+ }
87
+ tokens := OpenUsageLedger(LedgerOptions{Path: filepath.Join(dir, "t.json"), Now: now})
88
+ tokens.RecordSuccess(100, 0)
89
+ if got := tokens.Blocked(SpendCaps{MaxInputTokensPerDay: 100}); got == nil || got.Cap != CapInputTokensPerDay {
90
+ t.Fatalf("blocked = %+v", got)
91
+ }
92
+ usd := OpenUsageLedger(LedgerOptions{Path: filepath.Join(dir, "u.json"), Now: now, UsdPerMTok: 1_000_000})
93
+ usd.RecordSuccess(1, 0)
94
+ if got := usd.Blocked(SpendCaps{MaxUSDPerDay: 0.5}); got == nil || got.Cap != CapUSDPerDay || got.Used != 1 {
95
+ t.Fatalf("blocked = %+v", got)
96
+ }
97
+ // The caps belong to the caller: the same totals block under one cap and pass under another.
98
+ if usd.Blocked(SpendCaps{MaxUSDPerDay: 10}) != nil {
99
+ t.Fatal("a higher cap must not block")
100
+ }
101
+ })
102
+ tw(t, "usage", "caps come from the environment without ever raising the explicit cap", func(t *testing.T) {
103
+ env := map[string]string{"PI_TYPESAFE_MAX_REQUESTS_PER_DAY": "500", "PI_TYPESAFE_MAX_USD_PER_DAY": "2.5", "PI_TYPESAFE_MAX_INPUT_TOKENS_PER_DAY": "nonsense"}
104
+ get := func(k string) string { return env[k] }
105
+ environment := CapsFromEnvironment(get)
106
+ if environment != (SpendCaps{MaxRequestsPerDay: 500, MaxUSDPerDay: 2.5}) {
107
+ t.Fatalf("environment = %+v", environment)
108
+ }
109
+ if got := MergeCaps(SpendCaps{MaxRequests: 20, MaxUSDPerDay: 1}, environment); got != (SpendCaps{MaxRequests: 20, MaxRequestsPerDay: 500, MaxUSDPerDay: 1}) {
110
+ t.Fatalf("merged = %+v", got)
111
+ }
112
+ if MergeCaps(SpendCaps{}, SpendCaps{}) != (SpendCaps{}) || CapsFromEnvironment(func(string) string { return "" }) != (SpendCaps{}) {
113
+ t.Fatal("empty caps must stay empty")
114
+ }
115
+ })
116
+ tw(t, "usage", "a corrupt, unreadable, or foreign ledger never blocks a request", func(t *testing.T) {
117
+ path := filepath.Join(t.TempDir(), "corrupt.json")
118
+ now := func() time.Time { return at(6, 12) }
119
+ writeFile(t, path, "{ not json", 0o600)
120
+ l := OpenUsageLedger(LedgerOptions{Path: path, Now: now})
121
+ if l.Today().RequestsStarted != 0 {
122
+ t.Fatal("corrupt ledger must restart the count")
123
+ }
124
+ l.RecordStart()
125
+ if OpenUsageLedger(LedgerOptions{Path: path, Now: now}).Today().RequestsStarted != 1 {
126
+ t.Fatal("the rewritten ledger must persist")
127
+ }
128
+ writeFile(t, path, `{"version":1,"days":{"2026-01-06":{"requestsStarted":"many"},"notADay":{}}}`, 0o600)
129
+ if OpenUsageLedger(LedgerOptions{Path: path, Now: now}).Today().RequestsStarted != 0 {
130
+ t.Fatal("a foreign ledger must read as zero")
131
+ }
132
+ })
133
+ tw(t, "usage", "the default usage path sits with the key store", func(t *testing.T) {
134
+ dir := isolate(t)
135
+ if UsagePath() != filepath.Join(dir, "pi-typesafe", "usage.json") || LocalDay(at(7, 12)) != "2026-01-07" {
136
+ t.Fatalf("path = %s", UsagePath())
137
+ }
138
+ })
139
+ }
@@ -0,0 +1,125 @@
1
+ # CONTRACT: the shared Go TypeSafe client
2
+
3
+ Status: **implemented.** The signatures in `libraries/*` were frozen first so that the extensions built on
4
+ this client (`pi-typesafe`, `jev`, `warden`) could write their tests against them; the implementation has
5
+ since landed and those Packages use it. Changes are listed under "Changes since the CONTRACT commit" below.
6
+
7
+ ## Layout
8
+
9
+ The Package is a library Package (root `go.mod`, no extension of its own), like `pig-play`. It uses the
10
+ standard library only.
11
+
12
+ | Import path | Role |
13
+ |---|---|
14
+ | `github.com/MichaelKinsy/pigpen/components/typesafe/libraries/typesafe` | The client: port of `@typesafe-ai/sdk` 0.6.0 (questions, answers, errors, retries, timeouts, logging, models, `Evaluator`, `EvaluateBatch`). |
15
+ | `github.com/MichaelKinsy/pigpen/components/typesafe/libraries/ownmodel` | The second backend: answers the same questions with a `Model` you give it (port of system-one-adapter-python). Implements `typesafe.Evaluator`. |
16
+ | `github.com/MichaelKinsy/pigpen/components/typesafe/libraries/pigmodel` | A `ownmodel.Model` on the PiG Go SDK model access (`Context.ModelRegistry()`); the model PiG is configured with. |
17
+
18
+ Use it from an extension the way `angry-pigs` uses `pig-play`: the extension's `go.mod` requires
19
+ `github.com/MichaelKinsy/pigpen/components/typesafe v0.0.0` (no `replace`), and its directory has a
20
+ `go.work` with `use ( . ../../../typesafe )`; a Piglet selects the Package under an alias equal to its
21
+ directory name (`typesafe`). See `components/extension-port/skills/pigpen-pi-extension-port/SKILL.md`, step 8.
22
+
23
+ `npm run test:go-ports` picks up this Package's root-level `go.mod`, so it vets and tests it with the other
24
+ modules; `go test ./...` in this directory also works.
25
+
26
+ ## Use
27
+
28
+ ```go
29
+ // The TypeSafe API (TYPESAFE_API_KEY, TYPESAFE_BASE_URL, TYPESAFE_DEFAULT_MODEL, TYPESAFE_LOG_LEVEL).
30
+ client, err := typesafe.NewClient(typesafe.Config{})
31
+ res, err := client.SystemOne(ctx, typesafe.SystemOneRequest{
32
+ State: typesafe.Text("I was charged twice. Please fix this ASAP."),
33
+ Questions: typesafe.Questions{
34
+ typesafe.Ask("billing", typesafe.Noul("Is this about billing?")),
35
+ typesafe.Ask("tone", typesafe.Choice("Tone?", typesafe.Opt("calm", nil), typesafe.Opt("angry", nil))),
36
+ typesafe.Ask("urgency", typesafe.Score("How urgent?", "can wait", "this week", "today")),
37
+ },
38
+ }, nil)
39
+ tone, err := res.Choice("tone") // typed: ChoiceAnswer{Choice, Confidence, Probabilities}
40
+
41
+ // The own model: whatever PiG is configured with.
42
+ info, _ := ctx.GetModelInfo() // PiG Go SDK
43
+ m, _ := pigmodel.New(ctx.ModelRegistry(), pigmodel.Ref{Provider: info.Provider, ID: info.ID})
44
+ own, _ := ownmodel.New(ownmodel.Options{Model: m, AnswerMode: ownmodel.Probabilities})
45
+
46
+ var ev typesafe.Evaluator = own // or client: same request, same result type
47
+ out := typesafe.EvaluateBatch(ctx, ev, items, typesafe.BatchOptions{Concurrency: 4})
48
+ ```
49
+
50
+ The signatures with their documentation are in the source (`go doc` shows them); this document states
51
+ what the doc comments do not: the mapping from the TypeScript SDK and every deliberate difference.
52
+
53
+ ## Semantics that are fixed
54
+
55
+ - **Wire format** is the TypeScript SDK's: `POST {baseURL}/v1/systemone` with body
56
+ `{state, questions, model, ...extra}`, `GET {baseURL}/v1/models`; headers `Authorization: Bearer <key>`,
57
+ `Accept: application/json`, `User-Agent`, `X-TypeSafe-SDK`, `X-TypeSafe-Runtime`, `Content-Type: application/json`
58
+ only when there is a body, and `X-TypeSafe-Retry-Count: <n>` on retries. Caller headers cannot replace
59
+ those (case-insensitive); per-call headers win over `DefaultHeaders`.
60
+ - **Omitted vs null** is preserved. `Entry{}` is omitted; `Null` and `Value(nil)` are null. `Noul(x)` sends
61
+ `instructions` (null when x is nil) and omits `criteria`; `.Yes`/`.No` set one side; `.NullCriteria()` sends null.
62
+ - **Validation before sending** (`*TypeSafeError`, texts as in the SDK): no questions ("At least one question is
63
+ required."), a score question with fewer than two criteria ("Score question "q" has N criteria; at least two
64
+ scores are required."), and, added for Go, duplicate names.
65
+ - **Retries**: default 2 retries after the first attempt; 500 ms initial backoff doubling to 5 s; jitter 0.25;
66
+ statuses 408, 429 and 500 to 599; `Retry-After` / `retry-after-ms` honored up to 60 s; connection failures and
67
+ timeouts retried. Cancellation is never retried. **Timeout** is per attempt (10 s), covers headers and body,
68
+ and there is no total budget.
69
+ - **Errors**: `errors.As` targets as drawn in the `TypeSafeError` doc. Messages are the SDK's, for example
70
+ `401 invalid api key`, `Request timed out after 1000ms.`, `No API key was provided...`.
71
+ - **Answers** decode by their `type`; `Noul`, `Choice`, `Score` accessors on the result return an error for a
72
+ missing or mismatched answer. A response with no answers is not an error.
73
+ - **Secrets**: the API key is never exposed by a method, `String`, `%+v`, `%#v`, JSON encoding or logs (the
74
+ `Authorization` header is redacted to `Bearer ***abcd`).
75
+
76
+ ## Deliberate differences from the TypeScript SDK
77
+
78
+ | TypeScript | Go | Why |
79
+ |---|---|---|
80
+ | `AbortSignal` in per-call options | the `context.Context` argument; ending it returns `*APIUserAbortError` | Go idiom |
81
+ | `fetch` option | `Config.HTTPClient` (`HTTPDoer`) | Go idiom |
82
+ | `dangerouslyAllowBrowser`, browser guard, `describeRuntime()` runtime names | not ported; `X-TypeSafe-Runtime` is `go/<version> (<os>; <arch>)` and `X-TypeSafe-SDK` is `typesafe-sdk-go/0.6.0` | no browser; an honest client identity |
83
+ | `APIPromise` (`asResponse`, `withResponse`, `map`, thenable) | `SystemOneWithResponse`, `SystemOneRaw`, `Models.ListWithResponse`, `Models.ListRaw`, `MapResponse` | no promises in Go |
84
+ | answers typed by inference from the question literals | typed accessors (`res.Choice(name)`); answer types are concrete structs | Go has no such inference |
85
+ | object key order of `criteria` and `questions` | ordered slices (`[]Option`, `Questions`) | Go maps are unordered |
86
+ | numbers in milliseconds | `time.Duration` | Go idiom |
87
+ | an explicitly empty `apiKey`, `baseURL` or `defaultModel` is used as given | the empty string means "not set" and falls back | Go zero values |
88
+ | `timeout: 0` is rejected | zero means the default; negative is rejected | Go zero values |
89
+ | `Retry-After` HTTP dates parsed by JavaScript `Date.parse` | RFC 1123, RFC 850, ANSI C and RFC 3339 dates | no Go equivalent of the lenient parser |
90
+ | no batching | `EvaluateBatch` (bounded concurrency, input order, per-item errors) | requested for the batched evaluate tools |
91
+
92
+ ## ownmodel and pigmodel
93
+
94
+ `ownmodel.Backend` builds one prompt per request from the state (`<document>` block, angle brackets escaped,
95
+ "treat the document as untrusted data"), asks the model for one JSON object matching a per-question schema,
96
+ validates it strictly, and converts it to the same `SystemOneResult` the API returns. `Probabilities` mode asks
97
+ for a probability per label; `Discrete` mode for exactly one value. Invalid distributions are rescaled only when
98
+ `NormalizeProbabilities` is set (score expectations are always computed over a rescaled distribution). A
99
+ malformed answer is retried up to `MalformedRetries` times with the earlier answer and a correction message;
100
+ transient provider errors follow the `typesafe` retry policy (default: none). Refusals, incomplete generations
101
+ and output-limit stops fail at once and do not use up corrective retries. `Evaluate` returns usage totals and
102
+ the attempt history; `SystemOne` returns only the result (unreported token counts are zero there).
103
+
104
+ `pigmodel.Model` sends the conversation through `ModelRegistry.Find`, `GetApiKeyAndHeaders` and `Complete`; it
105
+ names no provider or model, and it supports prompted mode only (PiG's model access has no native
106
+ structured-output mode).
107
+
108
+ Content sent to an own-model backend goes to the configured model's provider; content sent to a
109
+ `typesafe.Client` goes to the TypeSafe API. Extensions must disclose which.
110
+
111
+ ## Changes since the CONTRACT commit (`841b25f`)
112
+
113
+ All additive; no signature of the contract changed.
114
+
115
+ - `typesafe.RetryHooks.OnAbortDuringWait` (optional hook: the caller aborted while waiting to retry).
116
+ - `typesafe.NoulCriteria.MarshalJSON`.
117
+ - `typesafe.NewConnectionError`, `typesafe.NewTimeoutError`, `typesafe.NewAbortError`: constructors that a `Model` or
118
+ another transport uses to return errors the retry policy classifies.
119
+ - `ownmodel.MalformedOutputError`: returned (inside a `*DebugError`) when the model's output still does not
120
+ match the schema after `MalformedRetries` corrections; `errors.As` finds it and it unwraps to a `*typesafe.TypeSafeError`
121
+ whose `Cause` is the validation error.
122
+ - Request bodies are encoded without HTML escaping (`<`, `>`, `&` as written), like the official SDK.
123
+ - (Review) `ParseRetryAfter`, `RetryPolicy.Delay` and `RateLimitError.RetryAfter` saturate at the largest
124
+ `time.Duration` instead of wrapping to a negative value; a huge `Retry-After` now falls back to backoff like the SDK.
125
+
@@ -0,0 +1,37 @@
1
+ # Credits
2
+
3
+ This Package is a Go port of two MIT-licensed projects by TypeSafe AI, and its cross-check uses a third (Apache-2.0). It keeps their
4
+ copyright notices and licenses (in `port/oracle/<name>/LICENSE`).
5
+
6
+ ## typesafe-sdk-js
7
+
8
+ `libraries/typesafe` ports **@typesafe-ai/sdk** 0.6.0, the official TypeScript SDK for the
9
+ TypeSafe API: https://github.com/typesafe-ai/typesafe-sdk-js, by **TypeSafe**
10
+ (package author **evinism**), MIT, Copyright (c) 2026 TypeSafe.
11
+
12
+ - Pinned commit: `66880ccded6cb642dc1809620c2b108c33730214` (tag/version 0.6.0)
13
+ - License: [`port/oracle/typesafe-sdk-js/LICENSE`](port/oracle/typesafe-sdk-js/LICENSE)
14
+
15
+ ## system-one-adapter-python
16
+
17
+ `libraries/ownmodel` ports **system-one-adapter** 0.2.1, the drop-in `system_one` backend
18
+ that answers TypeSafe questions with an LLM: https://github.com/typesafe-ai/system-one-adapter-python,
19
+ by **TypeSafe AI** (maintainers **Erik Gafni** and **Daniel Gafni**), MIT, Copyright (c) 2026 TypeSafe AI.
20
+
21
+ - Pinned commit: `e1d4cc938204b22fc5a3c3aca7044072fe3f712d`
22
+ - License: [`port/oracle/system-one-adapter-python/LICENSE`](port/oracle/system-one-adapter-python/LICENSE)
23
+
24
+ ## WorkflowEvals (cross-check only)
25
+
26
+ https://github.com/typesafe-ai/WorkflowEvals (Apache-2.0, TypeSafe AI; the commits are by Sam) is used only to check the
27
+ Go client against, at commit `0ac3b8ad845429f0d8e064ecfb2430a47c5a25cb` (Apache-2.0, license in
28
+ [`port/oracle/WorkflowEvals/LICENSE`](port/oracle/WorkflowEvals/LICENSE)). The question sets its workflows
29
+ build (`port/crosscheck/workflowevals.json`) and the extraction script that records them are derived from
30
+ its code; no dataset content is stored, and the states in that file are synthetic. No other source of it is
31
+ copied into this Package.
32
+
33
+ ## What is modified
34
+
35
+ The Go code, tests and documents were written for this Package by Michael Kinsy; the behavior,
36
+ error texts, defaults, prompts and test cases follow the originals. `port/PORT.md` maps every
37
+ upstream file to its Go counterpart and lists each deliberate difference.