@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,134 @@
1
+ package pi_typesafe_test
2
+
3
+ import (
4
+ "strings"
5
+ "sync"
6
+ "testing"
7
+
8
+ pi_typesafe "github.com/MichaelKinsy/pigpen/components/pi-typesafe/extensions/pi-typesafe"
9
+ )
10
+
11
+ // Cases added by the review (rev-pigpen-typesafe).
12
+
13
+ // Consent is per destination. A /typesafe backend switch may arrive while a tool call is running (PiG runs
14
+ // extension commands during a turn); the call that was admitted under the TypeSafe consent must not be sent
15
+ // to the own model, whose destination the operator has not consented to. It stops and says so.
16
+ func TestBackendSwitchAfterAdmissionKeepsTheConsentedDestination(t *testing.T) {
17
+ own := &fakeOwn{}
18
+ r := newRig(t, func(o *pi_typesafe.Options) { o.Evaluator = own })
19
+ t.Setenv("TYPESAFE_API_KEY", "offline-test-key")
20
+ t.Setenv("PI_TYPESAFE_ENABLED", "1")
21
+ r.startSession("startup")
22
+ var once sync.Once
23
+ var switchFailure string
24
+ restore := pi_typesafe.SetAdmittedHook(func() {
25
+ once.Do(func() { switchFailure = r.host.Command("typesafe", "backend ownmodel") })
26
+ })
27
+ defer restore()
28
+ _, failure := r.tool(nil)
29
+ if switchFailure != "" {
30
+ t.Fatalf("switch: %s", switchFailure)
31
+ }
32
+ if own.calls != 0 {
33
+ t.Fatalf("an admitted TypeSafe call reached the own model, a destination nobody consented to (calls=%d)", own.calls)
34
+ }
35
+ if !strings.Contains(failure, "nothing was sent") || r.network.Load() != 0 {
36
+ t.Fatalf("the call stops and says so: failure=%q network=%d", failure, r.network.Load())
37
+ }
38
+ if _, failure := r.tool(nil); !strings.Contains(failure, "disabled") || own.calls != 0 {
39
+ t.Fatalf("after the switch the tool is disabled: %q calls=%d", failure, own.calls)
40
+ }
41
+ }
42
+
43
+ // The same for /typesafe test: the operator confirmed sending to api.typesafe.ai; if the backend changed while
44
+ // the dialog was open, nothing is sent.
45
+ func TestSampleSendsNothingWhenTheBackendChangedDuringConsent(t *testing.T) {
46
+ own := &fakeOwn{}
47
+ r := newRig(t, func(o *pi_typesafe.Options) { o.Evaluator = own })
48
+ t.Setenv("TYPESAFE_API_KEY", "offline-test-key")
49
+ var once sync.Once
50
+ r.onConfirm = func() { once.Do(func() { _ = r.host.Command("typesafe", "backend ownmodel") }) }
51
+ r.command("test")
52
+ if !strings.Contains(r.confirmBodies[0], "api.typesafe.ai") {
53
+ t.Fatalf("consent body = %s", r.confirmBodies[0])
54
+ }
55
+ if own.calls != 0 || r.network.Load() != 0 || len(r.entries) != 0 {
56
+ t.Fatalf("nothing may be sent after the destination changed: own=%d network=%d entries=%d", own.calls, r.network.Load(), len(r.entries))
57
+ }
58
+ if !strings.Contains(r.lastNotice(), "nothing was sent") {
59
+ t.Fatalf("the operator is told: %s", r.lastNotice())
60
+ }
61
+ }
62
+
63
+ // The original reads the whole argument text as the action (`args.trim() || "status"`), so an action with
64
+ // trailing words is not an action: it gets the usage text, and nothing runs. Only this port's `backend`
65
+ // takes an argument.
66
+ func TestAnActionWithTrailingWordsGetsTheUsageText(t *testing.T) {
67
+ r := newRig(t)
68
+ t.Setenv("TYPESAFE_API_KEY", "offline-test-key")
69
+ for _, args := range []string{"status now", "enable yes", "disable all", "logout please", "test twice"} {
70
+ r.command(args)
71
+ if !strings.HasPrefix(r.lastNotice(), "Usage: /typesafe login | logout") {
72
+ t.Fatalf("/typesafe %s: %s", args, r.lastNotice())
73
+ }
74
+ }
75
+ if r.confirmations != 0 || r.network.Load() != 0 {
76
+ t.Fatalf("nothing runs: confirmations=%d network=%d", r.confirmations, r.network.Load())
77
+ }
78
+ r.command(" status ")
79
+ if !strings.HasPrefix(r.lastNotice(), "TypeSafe: disabled.") {
80
+ t.Fatalf("surrounding space is trimmed: %s", r.lastNotice())
81
+ }
82
+ r.command("backend ownmodel")
83
+ if !strings.Contains(r.lastNotice(), "Backend set to ownmodel") {
84
+ t.Fatalf("backend takes its argument: %s", r.lastNotice())
85
+ }
86
+ }
87
+
88
+ // Off by default: only the exact value 1 enables the tool from the environment, at construction and at
89
+ // every session start, as in the original (`process.env.PI_TYPESAFE_ENABLED === "1"`).
90
+ func TestOnlyTheExactValueOneEnablesFromTheEnvironment(t *testing.T) {
91
+ for _, value := range []string{"0", "true", "yes", " 1", "1 "} {
92
+ r := newRig(t, func(*pi_typesafe.Options) { t.Setenv("PI_TYPESAFE_ENABLED", value) })
93
+ t.Setenv("TYPESAFE_API_KEY", "offline-test-key")
94
+ if _, failure := r.tool(nil); !strings.Contains(failure, "TypeSafe is disabled") {
95
+ t.Fatalf("PI_TYPESAFE_ENABLED=%q at load enabled the tool: %q", value, failure)
96
+ }
97
+ r.startSession("reload")
98
+ if _, failure := r.tool(nil); !strings.Contains(failure, "TypeSafe is disabled") {
99
+ t.Fatalf("PI_TYPESAFE_ENABLED=%q at session start enabled the tool: %q", value, failure)
100
+ }
101
+ if r.network.Load() != 0 {
102
+ t.Fatalf("PI_TYPESAFE_ENABLED=%q sent a request", value)
103
+ }
104
+ }
105
+ r := newRig(t, func(*pi_typesafe.Options) { t.Setenv("PI_TYPESAFE_ENABLED", "1") })
106
+ t.Setenv("TYPESAFE_API_KEY", "offline-test-key")
107
+ if _, failure := r.tool(nil); failure != "" || r.network.Load() != 1 {
108
+ t.Fatalf("PI_TYPESAFE_ENABLED=1 enables: %q network=%d", failure, r.network.Load())
109
+ }
110
+ }
111
+
112
+ // The production own-model evaluator (hostmodel), not an injected one: with no model selected in PiG the
113
+ // call fails with the reason (not a generic "unreadable response"), the callout does not blame a key the
114
+ // backend does not use, and nothing is sent anywhere; status names the configured model.
115
+ func TestOwnModelBackendOnTheHostModel(t *testing.T) {
116
+ r := newRig(t, func(*pi_typesafe.Options) {
117
+ t.Setenv("PI_TYPESAFE_BACKEND", "ownmodel")
118
+ t.Setenv("PI_TYPESAFE_ENABLED", "1")
119
+ })
120
+ before := r.noticeCount()
121
+ _, failure := r.tool(nil)
122
+ if !strings.Contains(failure, "No model is configured in PiG; select one with /model first.") || r.network.Load() != 0 || r.modelList.Load() != 0 {
123
+ t.Fatalf("failure=%q network=%d", failure, r.network.Load())
124
+ }
125
+ said := strings.Join(r.noticesSince(before), "\n")
126
+ if !strings.Contains(said, "No model is configured in PiG") || strings.Contains(said, "key") || strings.Contains(said, "not authenticated") {
127
+ t.Fatalf("callout = %q", said)
128
+ }
129
+ r.modelInfo = map[string]any{"id": "model-one", "provider": "provider-one", "name": "Model One"}
130
+ r.command("status")
131
+ if !strings.Contains(r.lastNotice(), "Model: provider-one/model-one.") || !strings.Contains(r.lastNotice(), "Nothing is sent to api.typesafe.ai") {
132
+ t.Fatalf("status = %s", r.lastNotice())
133
+ }
134
+ }
@@ -0,0 +1,193 @@
1
+ package pi_typesafe
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "strings"
7
+
8
+ sdk "github.com/MichaelKinsy/PiG/extensions/sdk"
9
+ pitypesafe "github.com/MichaelKinsy/pigpen/components/pi-typesafe-api"
10
+ )
11
+
12
+ const ownModelMsg = "Submitted state and questions will be sent to the provider of the model PiG is configured with (the own-model backend) and may incur charges there. Nothing is sent to api.typesafe.ai. Do not include secrets. The extension does not collect files or conversation history. Results are model judgments, not proof or authorization."
13
+
14
+ // disclosure says where submitted content goes for a backend.
15
+ func disclosure(backend string) string {
16
+ if backend == pitypesafe.BackendOwnModel {
17
+ return ownModelMsg
18
+ }
19
+ return typesafeMsg
20
+ }
21
+
22
+ // sampleJSON is the request the playground edits and the tool guidelines show: the payload shape is what models get
23
+ // wrong on the first call, and it is paid for on every tool listing, so it stays short.
24
+ const sampleJSON = `{"state":{"message":"I was charged twice for my subscription. Please help today."},"questions":{` +
25
+ `"category":{"type":"choice","instructions":"Which team should handle this message?","criteria":{"billing":"Charges and payments","technical":"Software failures","other":"None of these"}},` +
26
+ `"urgent":{"type":"noul","instructions":"Does the sender request help today?"},` +
27
+ `"frustration":{"type":"score","instructions":"How frustrated does the sender sound?","criteria":["Neutral request","Frustrated but civil","Angry or threatening"]}}}`
28
+
29
+ func toolDescription(backend string) string {
30
+ return "Evaluate supplied state with independent Choice, Score, and Noul questions in one TypeSafe request. " +
31
+ "Each question judges the whole state, so when several items are involved, put each item in a named state field (e.g. `reports.r1`) and ask one question per item per dimension (e.g. `r1_owner`, `r2_owner`), naming the field in the instructions; never aggregate several items into one question. " +
32
+ disclosure(backend) + " Requires operator opt-in via /typesafe enable or PI_TYPESAFE_ENABLED=1. " +
33
+ fmt.Sprintf("Limit: 32 questions, %d KiB JSON, %d attempts per session; no retries.", pitypesafe.DefaultMaxInputBytes/1024, pitypesafe.DefaultMaxRequests)
34
+ }
35
+
36
+ func toolGuidelines() []string {
37
+ return []string{
38
+ // The payload shape is what models get wrong on the first call; the same sample the playground edits is the cheapest way to show it.
39
+ "Request shape, all three question kinds in one call: " + sampleJSON,
40
+ "Use typesafe_evaluate only for requested semantic judgments, not calculations or exact lookups; send only the relevant permitted data.",
41
+ "Batch independent typesafe_evaluate questions over the same state; use code or explicit permission rules for actions, never confidence as authorization.",
42
+ "When typesafe_evaluate judges several items, give each item a named state field and ask one question per item per dimension, naming the field in the instructions; one question over many items returns an unusable blend.",
43
+ "Report typesafe_evaluate answers as the model's judgments with their probabilities; do not replace them with your own guesses, and say when an answer is uncertain.",
44
+ }
45
+ }
46
+
47
+ // toMap converts an ordered tree into the maps the SDK's arguments use.
48
+ func toMap(v any) any {
49
+ switch t := v.(type) {
50
+ case *pitypesafe.Object:
51
+ m := make(map[string]any, t.Len())
52
+ for _, k := range t.Keys() {
53
+ x, _ := t.Get(k)
54
+ m[k] = toMap(x)
55
+ }
56
+ return m
57
+ case []any:
58
+ out := make([]any, len(t))
59
+ for i, e := range t {
60
+ out[i] = toMap(e)
61
+ }
62
+ return out
63
+ }
64
+ return v
65
+ }
66
+
67
+ // registerTool (re)registers typesafe_evaluate; registering a name again replaces the definition, which is how
68
+ // a backend switch updates the disclosure in the tool description.
69
+ func (s *state) registerTool() {
70
+ backend := s.currentBackend()
71
+ s.ext.RegisterTool(sdk.ToolDefinition{
72
+ Name: toolName,
73
+ Label: "TypeSafe",
74
+ Description: toolDescription(backend),
75
+ PromptSnippet: "Ask batched structured questions with TypeSafe (external service; operator opt-in required)",
76
+ PromptGuidelines: toolGuidelines(),
77
+ Parameters: pitypesafe.EvaluationSchema(),
78
+ // Pi validates against `parameters` after this hook.
79
+ PrepareArguments: func(params map[string]any) (map[string]any, error) {
80
+ if normalized, ok := toMap(pitypesafe.NormalizeEvaluationRequest(params)).(map[string]any); ok {
81
+ return normalized, nil
82
+ }
83
+ return params, nil
84
+ },
85
+ Execute: s.execute,
86
+ RenderCall: s.renderCall,
87
+ RenderResult: s.renderResult,
88
+ })
89
+ }
90
+
91
+ // admitted is a test seam (export_test.go): it runs after a tool call passed the consent gate and before
92
+ // anything is sent, where a concurrent /typesafe command could change the backend.
93
+ var admitted = func() {}
94
+
95
+ func (s *state) execute(ctx sdk.Context, params map[string]any) (any, error) {
96
+ enabled, consent := s.admit()
97
+ if !enabled {
98
+ return nil, &pitypesafe.IntegrationError{Code: pitypesafe.CodeConfiguration, Message: "TypeSafe is disabled. Ask the operator to run /typesafe enable; do not enable it by editing configuration or environment files."}
99
+ }
100
+ admitted()
101
+ // The tool admits through the same rule as the library; EvaluateRaw re-runs it idempotently.
102
+ request, err := pitypesafe.PrepareEvaluationRequest(params, pitypesafe.PrepareOptions{})
103
+ if err != nil {
104
+ return nil, err
105
+ }
106
+ client, err := s.clientFor(consent)
107
+ if err == errDestinationChanged {
108
+ return nil, err
109
+ }
110
+ if err != nil {
111
+ return nil, s.report(ctx, err)
112
+ }
113
+ goCtx, cancel := goContext(ctx)
114
+ defer cancel()
115
+ result, err := client.EvaluateRaw(goCtx, request)
116
+ if err != nil {
117
+ return nil, s.report(ctx, err)
118
+ }
119
+ content, err := result.MarshalJSON()
120
+ if err != nil {
121
+ return nil, err
122
+ }
123
+ details, err := result.DetailsJSON()
124
+ if err != nil {
125
+ return nil, err
126
+ }
127
+ return sdk.ToolResult{Content: string(content), Details: json.RawMessage(details)}, nil
128
+ }
129
+
130
+ // report classifies a failure and, when it means every later judgment is skipped, calls it out once.
131
+ func (s *state) report(ctx sdk.Context, err error) error {
132
+ safe := pitypesafe.SafeError(err, s.currentBackend())
133
+ // Authentication degradation is louder than a single failed call: it means every later judgment is skipped.
134
+ rejected := safe.Code == pitypesafe.CodeHTTP && (safe.Status == 401 || safe.Status == 403)
135
+ if rejected || safe.Code == pitypesafe.CodeConfiguration {
136
+ status := ""
137
+ if safe.Status != 0 {
138
+ status = fmt.Sprint(safe.Status)
139
+ }
140
+ text := "TypeSafe is not authenticated (" + safe.Message + ") Judgments will fail until the key is fixed."
141
+ if s.currentBackend() == pitypesafe.BackendOwnModel {
142
+ // The own-model backend has no key of this extension: name what failed instead.
143
+ text = "The own-model backend cannot answer (" + safe.Message + ") Judgments will fail until this is fixed."
144
+ }
145
+ s.callOut(ctx, "run:"+string(safe.Code)+":"+status, text)
146
+ }
147
+ return safe
148
+ }
149
+
150
+ func (s *state) renderCall(_ sdk.Context, args map[string]any, _ sdk.ToolRenderContext, width int) ([]string, error) {
151
+ questions, _ := args["questions"].(map[string]any)
152
+ where := "external request"
153
+ if s.currentBackend() == pitypesafe.BackendOwnModel {
154
+ where = "configured model"
155
+ }
156
+ return wrapText(fmt.Sprintf("TypeSafe · %d questions · %s", len(questions), where), width), nil
157
+ }
158
+
159
+ func (s *state) renderResult(_ sdk.Context, result sdk.ToolRenderResult, opts sdk.ToolRenderResultOptions, _ sdk.ToolRenderContext, width int) ([]string, error) {
160
+ if opts.IsPartial {
161
+ return wrapText("TypeSafe · waiting for response", width), nil
162
+ }
163
+ details, _ := result.Details.(map[string]any)
164
+ if _, ok := details["answers"].(map[string]any); !ok {
165
+ var parts []string
166
+ for _, block := range result.Content {
167
+ if block["type"] == "text" {
168
+ text, _ := block["text"].(string)
169
+ parts = append(parts, text)
170
+ }
171
+ }
172
+ return wrapText(strings.Join(parts, "\n"), width), nil
173
+ }
174
+ return wrapText(format(details, opts.Expanded), width), nil
175
+ }
176
+
177
+ func (s *state) renderEntry(_ sdk.Context, entry map[string]any, opts sdk.EntryRenderOptions, width int) ([]string, error) {
178
+ data, _ := entry["data"].(map[string]any)
179
+ if data == nil {
180
+ return wrapText("TypeSafe · no result", width), nil
181
+ }
182
+ return wrapText(format(data, opts.Expanded), width), nil
183
+ }
184
+
185
+ // ToolDescription is the typesafe_evaluate description for a backend: the original's text, with the own-model
186
+ // disclosure when that backend is selected.
187
+ func ToolDescription(backend string) string { return toolDescription(backend) }
188
+
189
+ // ToolGuidelines are the prompt guidelines of typesafe_evaluate, the request-shape example first.
190
+ func ToolGuidelines() []string { return toolGuidelines() }
191
+
192
+ // Disclosure says where submitted content goes for a backend.
193
+ func Disclosure(backend string) string { return disclosure(backend) }
@@ -0,0 +1,28 @@
1
+ package pi_typesafe_test
2
+
3
+ // Twin helpers for a port whose original has tests. Every upstream test case gets one
4
+ // tw(...) call carrying the upstream title unchanged, or one tskip(...) with the reason it
5
+ // cannot be ported. `pigeq twins check` reads these calls and fails on a missing title, an
6
+ // unknown one (a misspelling) or one reason shared by many skips.
7
+ //
8
+ // pigeq twins list --tests <upstream>/test > port/upstream-tests.json
9
+ // pigeq twins check --ledger port/upstream-tests.json --go extensions/<name> [--files a,b]
10
+ //
11
+ // Copy this file into the port as twin_test.go and replace pi_typesafe_test with the package name (in
12
+ // an external test package use `pi_typesafe_test_test`). A parametrized upstream title (it contains
13
+ // `${`) is twinned once, with the template text unchanged.
14
+
15
+ import "testing"
16
+
17
+ // tw runs one exact twin: file is the upstream test file (its name without .test.mjs), title
18
+ // the upstream test title, unchanged.
19
+ func tw(t *testing.T, file, title string, fn func(t *testing.T)) {
20
+ t.Helper()
21
+ t.Run(title, fn)
22
+ }
23
+
24
+ // tskip records a named gap: the upstream case has no Go twin, with its own reason.
25
+ func tskip(t *testing.T, file, title, reason string) {
26
+ t.Helper()
27
+ t.Run(title, func(t *testing.T) { t.Skip(reason) })
28
+ }
@@ -0,0 +1,14 @@
1
+ # Credits
2
+
3
+ This Package is a Go port of **pi-typesafe** 0.8.0, https://github.com/DevMortimer/pi-typesafe, by
4
+ **Ryan Gapac** (MIT, Copyright (c) 2026 Ryan Gapac). It is an independent project, not affiliated with TypeSafe AI or
5
+ the Pi authors, and so is this port.
6
+
7
+ - Pinned commit: `ed439f834665ad6fc652787f5ba7c23852566d49` (release 0.8.0, "Add Command Code backend and caller-supplied endpoints")
8
+ - The unmodified original, with its tests, is vendored in `components/pi-typesafe/port/oracle/`; its license is in `LICENSE`
9
+ of this Package and in `components/pi-typesafe/port/oracle/LICENSE`.
10
+ - What is modified: everything. The behavior, wording and structure follow the original; the code is a Go
11
+ implementation over the shared TypeSafe client (`components/typesafe`, a port of the official `@typesafe-ai/sdk`).
12
+ The mapping from each original file to its Go file is in `components/pi-typesafe/port/PORT.md`, with every deliberate difference.
13
+
14
+ The Go code, scenarios and tests were written for Pigpen by Michael Kinsy.
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ryan Gapac
4
+ Copyright (c) 2026 Michael Kinsy (the Go port)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,30 @@
1
+ # pi-typesafe-api (typed Go API)
2
+
3
+ The library half of the [pi-typesafe](../pi-typesafe/README.md) port: what an extension author imports to ask Jev typed
4
+ questions safely. Go module `github.com/MichaelKinsy/pigpen/components/pi-typesafe-api`, package `pitypesafe`, over the shared
5
+ client `components/typesafe`. A consumer extension's `go.mod` requires it at `v0.0.0` (no `replace`) and its directory has a
6
+ `go.work` with `use ( . ../../../pi-typesafe-api ../../../typesafe )`.
7
+
8
+ ```go
9
+ client, err := pitypesafe.New(pitypesafe.Options{MaxRequests: 5, MaxUSDPerDay: 1}) // key: TYPESAFE_API_KEY, else the login store
10
+ answer := pitypesafe.Ask(ctx, client, typesafe.SystemOneRequest{
11
+ State: typesafe.Text("I was charged twice."),
12
+ Questions: typesafe.Questions{typesafe.Ask("billing", typesafe.Noul("Is this about billing?"))},
13
+ }, pitypesafe.AskOptions{Timeout: 5 * time.Second})
14
+ if !answer.OK { /* answer.ErrorCode == pitypesafe.CodeBudget: stop asking; never an error value */ }
15
+ ```
16
+
17
+ | Piece | Where |
18
+ |---|---|
19
+ | Admission (`PrepareEvaluationRequest`: normalize model near-misses, validate, byte budget), `EvaluationSchema` | `schema.go`, `evaluation_schema.json` |
20
+ | Client with request budget, day caps, ledger, auth recording (`New`, `Evaluate`, `EvaluateRaw`, `ListModels`, `GetSpend`) | `client.go`, `usage.go`, `auth.go` |
21
+ | Backends: `typesafe`, `openrouter`, `commandcode`, a caller-supplied endpoint, and `ownmodel` | `backends.go` |
22
+ | Key store, key situations, `AgentDir` (PiG's directory) | `credentials.go` |
23
+ | `Ask`, `EvaluateMany`, `EvaluateAll`, `ChunkRequest`, `FanOut` | `ask.go`, `batch.go` |
24
+ | Calibration kit (`Calibrate`, `Replay`, `AUC`, ...) | `calibrate.go` |
25
+ | Own model on the PiG SDK's model access | `hostmodel/` |
26
+ | Hidden-input key prompt, `LoginWithPrompt`, `EnsureAPIKey` | `ui/` |
27
+
28
+ **Where content goes.** TypeSafe, OpenRouter, Command Code and custom-endpoint backends send it to that host
29
+ (`BackendHost` names it for consent text); the `ownmodel` backend sends it to the provider of the model PiG is configured with. A
30
+ key for one backend is never sent to another. Credit and license: [CREDITS.md](CREDITS.md). Mapping: [port/PORT.md](port/PORT.md).
@@ -0,0 +1,62 @@
1
+ package pitypesafe
2
+
3
+ import (
4
+ "context"
5
+ "time"
6
+
7
+ "github.com/MichaelKinsy/pigpen/components/typesafe/libraries/typesafe"
8
+ )
9
+
10
+ // Judge is anything with a client's Evaluate: the real client in a session, a stub in tests.
11
+ type Judge interface {
12
+ Evaluate(ctx context.Context, request typesafe.SystemOneRequest) (*Evaluation, error)
13
+ }
14
+
15
+ // DefaultAskTimeout is the default per-ask deadline; it matches the client's own request timeout.
16
+ const DefaultAskTimeout = 15 * time.Second
17
+
18
+ const askFallbackMessage = "TypeSafe request failed."
19
+
20
+ // AskOptions configure Ask.
21
+ type AskOptions struct {
22
+ // Timeout is the per-ask deadline, merged with the caller's own context. Default: DefaultAskTimeout.
23
+ Timeout time.Duration
24
+ }
25
+
26
+ // AskAnswer is the outcome of Ask.
27
+ type AskAnswer struct {
28
+ OK bool
29
+ // The fields below are set when OK.
30
+ Answers map[string]typesafe.Answer
31
+ Order []string
32
+ Model string
33
+ Usage typesafe.Usage
34
+ ElapsedMs int64
35
+ // Error and ErrorCode are set when not OK; ErrorCode is empty for a failure that is not an IntegrationError.
36
+ Error string
37
+ ErrorCode ErrorCode
38
+ }
39
+
40
+ // Ask is one typed Jev request that never fails: a failure comes back as an AskAnswer with OK false, this
41
+ // package's own message (which carries no upstream body, header, key, or submitted state) and its code, so a
42
+ // caller can stop asking after a budget error. The per-ask timeout is merged into the caller's context, so
43
+ // either can cancel the request.
44
+ //
45
+ // This is the author-facing "ask Jev" seam. Agents get the same admission through the typesafe_evaluate tool;
46
+ // the two share PrepareEvaluationRequest, so what one accepts the other accepts.
47
+ func Ask(ctx context.Context, judge Judge, request typesafe.SystemOneRequest, opts AskOptions) AskAnswer {
48
+ timeout := opts.Timeout
49
+ if timeout <= 0 {
50
+ timeout = DefaultAskTimeout
51
+ }
52
+ ctx, cancel := context.WithTimeout(ctx, timeout)
53
+ defer cancel()
54
+ result, err := judge.Evaluate(ctx, request)
55
+ if err != nil {
56
+ if ie, ok := err.(*IntegrationError); ok {
57
+ return AskAnswer{Error: ie.Message, ErrorCode: ie.Code}
58
+ }
59
+ return AskAnswer{Error: askFallbackMessage}
60
+ }
61
+ return AskAnswer{OK: true, Answers: result.Answers, Order: result.Order, Model: result.Model, Usage: result.Usage, ElapsedMs: result.ElapsedMs}
62
+ }
@@ -0,0 +1,76 @@
1
+ package pitypesafe
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "errors"
7
+ "reflect"
8
+ "testing"
9
+ "time"
10
+
11
+ "github.com/MichaelKinsy/pigpen/components/typesafe/libraries/typesafe"
12
+ )
13
+
14
+ type judgeFunc func(ctx context.Context, r typesafe.SystemOneRequest) (*Evaluation, error)
15
+
16
+ func (f judgeFunc) Evaluate(ctx context.Context, r typesafe.SystemOneRequest) (*Evaluation, error) {
17
+ return f(ctx, r)
18
+ }
19
+
20
+ func TestAsk(t *testing.T) {
21
+ tw(t, "ask", "a successful ask returns the typed answers, model, usage, and duration", func(t *testing.T) {
22
+ judge := judgeFunc(func(context.Context, typesafe.SystemOneRequest) (*Evaluation, error) {
23
+ return &Evaluation{SystemOneResult: &typesafe.SystemOneResult{Model: "jev-test", Answers: map[string]typesafe.Answer{"yes": typesafe.NoulAnswer{Noul: 0.9}}, Usage: typesafe.Usage{InputTokens: 12}}, ElapsedMs: 7, Order: []string{"yes"}}, nil
24
+ })
25
+ a := Ask(context.Background(), judge, sampleRequest(), AskOptions{})
26
+ yes, _ := a.Answers["yes"].(typesafe.NoulAnswer)
27
+ if !a.OK || yes.Noul != 0.9 || a.Model != "jev-test" || a.Usage.InputTokens != 12 || a.ElapsedMs != 7 {
28
+ t.Fatalf("answer = %+v", a)
29
+ }
30
+ })
31
+ tw(t, "ask", "a typed failure comes back as data, with pi-typesafe's own code and message", func(t *testing.T) {
32
+ fail := func(err error) Judge {
33
+ return judgeFunc(func(context.Context, typesafe.SystemOneRequest) (*Evaluation, error) { return nil, err })
34
+ }
35
+ budget := Ask(context.Background(), fail(newError(CodeBudget, "TypeSafe request limit reached (1 attempts per client instance).")), sampleRequest(), AskOptions{})
36
+ if !reflect.DeepEqual(budget, AskAnswer{Error: "TypeSafe request limit reached (1 attempts per client instance).", ErrorCode: CodeBudget}) {
37
+ t.Fatalf("budget = %+v", budget)
38
+ }
39
+ e := &IntegrationError{Code: CodeHTTP, Message: "TypeSafe returned HTTP 401. Check TYPESAFE_API_KEY. No automatic retry was made.", Status: 401}
40
+ if h := Ask(context.Background(), fail(e), sampleRequest(), AskOptions{}); h.OK || h.ErrorCode != CodeHTTP {
41
+ t.Fatalf("http = %+v", h)
42
+ }
43
+ })
44
+ tw(t, "ask", "an unknown failure is replaced by a fixed message so nothing from the transport escapes", func(t *testing.T) {
45
+ judge := judgeFunc(func(context.Context, typesafe.SystemOneRequest) (*Evaluation, error) {
46
+ return nil, errors.New("upstream body with a key: sk-secret")
47
+ })
48
+ a := Ask(context.Background(), judge, sampleRequest(), AskOptions{})
49
+ if !reflect.DeepEqual(a, AskAnswer{Error: "TypeSafe request failed."}) {
50
+ t.Fatalf("answer = %+v", a)
51
+ }
52
+ if data, _ := json.Marshal(a); contains(string(data), "sk-secret") {
53
+ t.Fatal("secret escaped")
54
+ }
55
+ })
56
+ tw(t, "ask", "the ask deadline and the caller's signal both cancel the request", func(t *testing.T) {
57
+ slow := judgeFunc(func(ctx context.Context, _ typesafe.SystemOneRequest) (*Evaluation, error) {
58
+ <-ctx.Done()
59
+ return nil, newError(CodeAborted, "TypeSafe request cancelled before submission.")
60
+ })
61
+ timedOut := Ask(context.Background(), slow, sampleRequest(), AskOptions{Timeout: 5 * time.Millisecond})
62
+ if !reflect.DeepEqual(timedOut, AskAnswer{Error: "TypeSafe request cancelled before submission.", ErrorCode: CodeAborted}) {
63
+ t.Fatalf("timed out = %+v", timedOut)
64
+ }
65
+ ctx, cancel := context.WithCancel(context.Background())
66
+ done := make(chan AskAnswer, 1)
67
+ go func() { done <- Ask(ctx, slow, sampleRequest(), AskOptions{Timeout: 5 * time.Second}) }()
68
+ cancel()
69
+ if a := <-done; a.OK {
70
+ t.Fatal("a cancelled ask must fail")
71
+ }
72
+ if DefaultAskTimeout != 15*time.Second {
73
+ t.Errorf("default = %v", DefaultAskTimeout)
74
+ }
75
+ })
76
+ }