@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.
- package/CREDITS.md +14 -0
- package/LICENSE +22 -0
- package/README.md +45 -0
- package/extensions/pi-typesafe/branches_test.go +185 -0
- package/extensions/pi-typesafe/command.go +319 -0
- package/extensions/pi-typesafe/export_test.go +9 -0
- package/extensions/pi-typesafe/extension.go +188 -0
- package/extensions/pi-typesafe/extension_test.go +321 -0
- package/extensions/pi-typesafe/fakehost_test.go +548 -0
- package/extensions/pi-typesafe/format.go +191 -0
- package/extensions/pi-typesafe/format_test.go +75 -0
- package/extensions/pi-typesafe/go.mod +10 -0
- package/extensions/pi-typesafe/go.sum +2 -0
- package/extensions/pi-typesafe/go.work +11 -0
- package/extensions/pi-typesafe/harness_test.go +200 -0
- package/extensions/pi-typesafe/ownmodel_test.go +100 -0
- package/extensions/pi-typesafe/review_test.go +134 -0
- package/extensions/pi-typesafe/tool.go +193 -0
- package/extensions/pi-typesafe/twin_test.go +28 -0
- package/libs/pi-typesafe-api/CREDITS.md +14 -0
- package/libs/pi-typesafe-api/LICENSE +22 -0
- package/libs/pi-typesafe-api/README.md +30 -0
- package/libs/pi-typesafe-api/ask.go +62 -0
- package/libs/pi-typesafe-api/ask_test.go +76 -0
- package/libs/pi-typesafe-api/auth.go +249 -0
- package/libs/pi-typesafe-api/auth_test.go +131 -0
- package/libs/pi-typesafe-api/backends.go +336 -0
- package/libs/pi-typesafe-api/backends_test.go +404 -0
- package/libs/pi-typesafe-api/batch.go +202 -0
- package/libs/pi-typesafe-api/batch_test.go +202 -0
- package/libs/pi-typesafe-api/battery_test.go +41 -0
- package/libs/pi-typesafe-api/calibrate.go +354 -0
- package/libs/pi-typesafe-api/calibrate_test.go +186 -0
- package/libs/pi-typesafe-api/client.go +615 -0
- package/libs/pi-typesafe-api/client_test.go +490 -0
- package/libs/pi-typesafe-api/credentials.go +252 -0
- package/libs/pi-typesafe-api/credentials_test.go +216 -0
- package/libs/pi-typesafe-api/doc.go +14 -0
- package/libs/pi-typesafe-api/errors.go +143 -0
- package/libs/pi-typesafe-api/evaluation.go +86 -0
- package/libs/pi-typesafe-api/evaluation_schema.json +264 -0
- package/libs/pi-typesafe-api/gaps_test.go +77 -0
- package/libs/pi-typesafe-api/go.mod +9 -0
- package/libs/pi-typesafe-api/go.sum +2 -0
- package/libs/pi-typesafe-api/helpers_test.go +169 -0
- package/libs/pi-typesafe-api/hostmodel/hostmodel.go +87 -0
- package/libs/pi-typesafe-api/json.go +299 -0
- package/libs/pi-typesafe-api/json_test.go +92 -0
- package/libs/pi-typesafe-api/ownmodel_test.go +79 -0
- package/libs/pi-typesafe-api/package.json +40 -0
- package/libs/pi-typesafe-api/provenance.json +18 -0
- package/libs/pi-typesafe-api/review_test.go +23 -0
- package/libs/pi-typesafe-api/schema.go +473 -0
- package/libs/pi-typesafe-api/schema_test.go +262 -0
- package/libs/pi-typesafe-api/testdata/tools/typebox-messages.mts +5 -0
- package/libs/pi-typesafe-api/testdata/typebox-messages.json +285 -0
- package/libs/pi-typesafe-api/twin_test.go +28 -0
- package/libs/pi-typesafe-api/ui/fakehost_test.go +548 -0
- package/libs/pi-typesafe-api/ui/keyprompt.go +115 -0
- package/libs/pi-typesafe-api/ui/login.go +106 -0
- package/libs/pi-typesafe-api/ui/twin_test.go +28 -0
- package/libs/pi-typesafe-api/ui/ui_test.go +285 -0
- package/libs/pi-typesafe-api/usage.go +366 -0
- package/libs/pi-typesafe-api/usage_test.go +139 -0
- package/libs/typesafe/CONTRACT.md +125 -0
- package/libs/typesafe/CREDITS.md +37 -0
- package/libs/typesafe/LICENSE +23 -0
- package/libs/typesafe/README.md +19 -0
- package/libs/typesafe/go.mod +3 -0
- package/libs/typesafe/libraries/ownmodel/backend_test.go +496 -0
- package/libs/typesafe/libraries/ownmodel/canon.go +190 -0
- package/libs/typesafe/libraries/ownmodel/convert.go +199 -0
- package/libs/typesafe/libraries/ownmodel/doc.go +15 -0
- package/libs/typesafe/libraries/ownmodel/equivalence_test.go +199 -0
- package/libs/typesafe/libraries/ownmodel/helpers_test.go +155 -0
- package/libs/typesafe/libraries/ownmodel/mutation_test.go +31 -0
- package/libs/typesafe/libraries/ownmodel/ownmodel.go +225 -0
- package/libs/typesafe/libraries/ownmodel/plan.go +442 -0
- package/libs/typesafe/libraries/ownmodel/run.go +288 -0
- package/libs/typesafe/libraries/ownmodel/schema_test.go +254 -0
- package/libs/typesafe/libraries/ownmodel/twins_test.go +169 -0
- package/libs/typesafe/libraries/ownmodel/utils_test.go +125 -0
- package/libs/typesafe/libraries/pigmodel/pigmodel.go +264 -0
- package/libs/typesafe/libraries/pigmodel/pigmodel_test.go +410 -0
- package/libs/typesafe/libraries/typesafe/answers.go +268 -0
- package/libs/typesafe/libraries/typesafe/api_response_test.go +113 -0
- package/libs/typesafe/libraries/typesafe/batch.go +80 -0
- package/libs/typesafe/libraries/typesafe/batch_test.go +133 -0
- package/libs/typesafe/libraries/typesafe/bench_test.go +71 -0
- package/libs/typesafe/libraries/typesafe/client.go +561 -0
- package/libs/typesafe/libraries/typesafe/client_test.go +495 -0
- package/libs/typesafe/libraries/typesafe/crosscheck_test.go +464 -0
- package/libs/typesafe/libraries/typesafe/crosscheck_workflowevals_test.go +219 -0
- package/libs/typesafe/libraries/typesafe/doc.go +27 -0
- package/libs/typesafe/libraries/typesafe/entry.go +142 -0
- package/libs/typesafe/libraries/typesafe/env.go +11 -0
- package/libs/typesafe/libraries/typesafe/errors.go +310 -0
- package/libs/typesafe/libraries/typesafe/errors_test.go +175 -0
- package/libs/typesafe/libraries/typesafe/helpers_test.go +294 -0
- package/libs/typesafe/libraries/typesafe/live_test.go +96 -0
- package/libs/typesafe/libraries/typesafe/logging.go +160 -0
- package/libs/typesafe/libraries/typesafe/logging_test.go +259 -0
- package/libs/typesafe/libraries/typesafe/marshal_test.go +112 -0
- package/libs/typesafe/libraries/typesafe/mutation_test.go +39 -0
- package/libs/typesafe/libraries/typesafe/questions.go +490 -0
- package/libs/typesafe/libraries/typesafe/questions_test.go +166 -0
- package/libs/typesafe/libraries/typesafe/regressions_test.go +159 -0
- package/libs/typesafe/libraries/typesafe/reliability_test.go +649 -0
- package/libs/typesafe/libraries/typesafe/retry.go +350 -0
- package/libs/typesafe/libraries/typesafe/retry_test.go +297 -0
- package/libs/typesafe/libraries/typesafe/runtime_test.go +26 -0
- package/libs/typesafe/libraries/typesafe/transport_test.go +163 -0
- package/libs/typesafe/libraries/typesafe/twins_test.go +127 -0
- package/libs/typesafe/libraries/typesafe/types_test.go +165 -0
- package/libs/typesafe/libraries/typesafe/version.go +10 -0
- package/libs/typesafe/package.json +37 -0
- package/libs/typesafe/provenance.json +49 -0
- package/package.json +42 -0
- package/port/PORT.md +98 -0
- package/port/accepted-gaps.json +3 -0
- package/port/golden/enable-confirm.jsonl +11 -0
- package/port/golden/enable-decline.jsonl +20 -0
- package/port/golden/enable-missing-key.jsonl +4 -0
- package/port/golden/login-shadow.jsonl +4 -0
- package/port/golden/logout-env-key.jsonl +6 -0
- package/port/golden/playground-cancel.jsonl +4 -0
- package/port/golden/playground-invalid-json.jsonl +5 -0
- package/port/golden/playground-invalid-questions.jsonl +5 -0
- package/port/golden/status-env-key.jsonl +6 -0
- package/port/golden/status-no-key.jsonl +6 -0
- package/port/golden/tool-disabled.jsonl +18 -0
- package/port/golden/trailing-words.jsonl +10 -0
- package/port/library-mutations.py +44 -0
- package/port/mutations.json +302 -0
- package/port/oracle/.env.example +4 -0
- package/port/oracle/CHANGELOG.md +91 -0
- package/port/oracle/CONTRIBUTING.md +35 -0
- package/port/oracle/LICENSE +21 -0
- package/port/oracle/README.md +159 -0
- package/port/oracle/docs/api.md +143 -0
- package/port/oracle/docs/ci-cd.md +97 -0
- package/port/oracle/examples/decision-extension.ts +41 -0
- package/port/oracle/extensions/index.js +2 -0
- package/port/oracle/package.json +89 -0
- package/port/oracle/scripts/dev-pi.mjs +23 -0
- package/port/oracle/scripts/live-smoke.mjs +35 -0
- package/port/oracle/src/ask.ts +42 -0
- package/port/oracle/src/auth.ts +171 -0
- package/port/oracle/src/backends.ts +196 -0
- package/port/oracle/src/batch.ts +170 -0
- package/port/oracle/src/calibrate.ts +237 -0
- package/port/oracle/src/client.ts +310 -0
- package/port/oracle/src/credentials.ts +136 -0
- package/port/oracle/src/errors.ts +53 -0
- package/port/oracle/src/extension.ts +204 -0
- package/port/oracle/src/index.ts +31 -0
- package/port/oracle/src/key-prompt.ts +51 -0
- package/port/oracle/src/login.ts +60 -0
- package/port/oracle/src/schema.ts +158 -0
- package/port/oracle/src/ui.ts +4 -0
- package/port/oracle/src/usage.ts +258 -0
- package/port/oracle/tests/ask.test.ts +63 -0
- package/port/oracle/tests/auth.test.ts +141 -0
- package/port/oracle/tests/backends.test.ts +380 -0
- package/port/oracle/tests/batch.test.ts +156 -0
- package/port/oracle/tests/calibrate.test.ts +144 -0
- package/port/oracle/tests/client.test.ts +499 -0
- package/port/oracle/tests/credentials.test.ts +144 -0
- package/port/oracle/tests/extension.test.ts +276 -0
- package/port/oracle/tests/key-prompt.test.ts +47 -0
- package/port/oracle/tests/login.test.ts +101 -0
- package/port/oracle/tests/schema.test.ts +85 -0
- package/port/oracle/tests/usage.test.ts +106 -0
- package/port/oracle/tsconfig.build.json +10 -0
- package/port/oracle/tsconfig.json +14 -0
- package/port/scenarios/enable-confirm.json +5 -0
- package/port/scenarios/enable-decline.json +3 -0
- package/port/scenarios/enable-missing-key.json +2 -0
- package/port/scenarios/login-shadow.json +2 -0
- package/port/scenarios/logout-env-key.json +3 -0
- package/port/scenarios/playground-cancel.json +2 -0
- package/port/scenarios/playground-invalid-json.json +2 -0
- package/port/scenarios/playground-invalid-questions.json +2 -0
- package/port/scenarios/status-env-key.json +3 -0
- package/port/scenarios/status-no-key.json +3 -0
- package/port/scenarios/tool-disabled.json +2 -0
- package/port/scenarios/trailing-words.json +5 -0
- package/port/upstream-tests.json +160 -0
- 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
|
+
}
|