Merge pull request #193 from airtonix/issue-174-interactive-timeout
fix(extensions): preserve interactive tool timeout policy (closes #174)
3 files · +65 -0
M packages/agent/build.go +1 -0
| @@ -132,6 +132,7 @@ type ExtensionToolInfo struct { | |||
| 132 | 132 | Description string | |
| 133 | 133 | Schema []byte | |
| 134 | 134 | Deferred bool | |
| 135 | + | Interactive bool | |
| 135 | 136 | } | |
| 136 | 137 | ||
| 137 | 138 | // toolSummariesFromRegistry rebuilds the system-prompt tool list | |
| @@ -132,6 +132,7 @@ type ExtensionToolInfo struct { | |||||
| 132 | Description string | 132 | Description string | ||
| 133 | Schema []byte | 133 | Schema []byte | ||
| 134 | Deferred bool | 134 | Deferred bool | ||
| 135 | + | Interactive bool | |||
| 135 | } | 136 | } | ||
| 136 | 137 | ||||
| 137 | // toolSummariesFromRegistry rebuilds the system-prompt tool list | 138 | // toolSummariesFromRegistry rebuilds the system-prompt tool list | ||
M packages/agent/cli.go +2 -0
| @@ -109,6 +109,7 @@ func (a *extToolAdapter) Tools() []ExtensionToolInfo { | |||
| 109 | 109 | Description: t.Description, | |
| 110 | 110 | Schema: t.Schema, | |
| 111 | 111 | Deferred: t.Deferred, | |
| 112 | + | Interactive: t.Interactive, | |
| 112 | 113 | } | |
| 113 | 114 | } | |
| 114 | 115 | return out | |
| @@ -121,6 +122,7 @@ func (a *extToolAdapter) NewExtensionTool(info ExtensionToolInfo) core.Tool { | |||
| 121 | 122 | Description: info.Description, | |
| 122 | 123 | Schema: info.Schema, | |
| 123 | 124 | Deferred: info.Deferred, | |
| 125 | + | Interactive: info.Interactive, | |
| 124 | 126 | }) | |
| 125 | 127 | } | |
| 126 | 128 | ||
| @@ -109,6 +109,7 @@ func (a *extToolAdapter) Tools() []ExtensionToolInfo { | |||||
| 109 | Description: t.Description, | 109 | Description: t.Description, | ||
| 110 | Schema: t.Schema, | 110 | Schema: t.Schema, | ||
| 111 | Deferred: t.Deferred, | 111 | Deferred: t.Deferred, | ||
| 112 | + | Interactive: t.Interactive, | |||
| 112 | } | 113 | } | ||
| 113 | } | 114 | } | ||
| 114 | return out | 115 | return out | ||
| @@ -121,6 +122,7 @@ func (a *extToolAdapter) NewExtensionTool(info ExtensionToolInfo) core.Tool { | |||||
| 121 | Description: info.Description, | 122 | Description: info.Description, | ||
| 122 | Schema: info.Schema, | 123 | Schema: info.Schema, | ||
| 123 | Deferred: info.Deferred, | 124 | Deferred: info.Deferred, | ||
| 125 | + | Interactive: info.Interactive, | |||
| 124 | }) | 126 | }) | ||
| 125 | } | 127 | } | ||
| 126 | 128 | ||||
A packages/agent/cli_extension_tools_test.go +62 -0
| @@ -0,0 +1,62 @@ | |||
| 1 | + | package agent | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "encoding/json" | |
| 6 | + | "os" | |
| 7 | + | "path/filepath" | |
| 8 | + | "runtime" | |
| 9 | + | "testing" | |
| 10 | + | "time" | |
| 11 | + | ||
| 12 | + | "github.com/patriceckhart/zot/packages/agent/extensions" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | func TestExtToolAdapterPreservesInteractiveTimeoutPolicy(t *testing.T) { | |
| 16 | + | if runtime.GOOS == "windows" { | |
| 17 | + | t.Skip("mock uses /bin/sh; skip on windows") | |
| 18 | + | } | |
| 19 | + | root := t.TempDir() | |
| 20 | + | extDir := filepath.Join(root, "extensions", "adapter-mock") | |
| 21 | + | if err := os.MkdirAll(extDir, 0o755); err != nil { | |
| 22 | + | t.Fatal(err) | |
| 23 | + | } | |
| 24 | + | script := `#!/bin/sh | |
| 25 | + | printf '%s\n' '{"type":"hello","name":"adapter-mock","version":"0.1","capabilities":["tools"]}' | |
| 26 | + | printf '%s\n' '{"type":"register_tool","name":"ask","description":"interactive","schema":{"type":"object"},"interactive":true}' | |
| 27 | + | printf '%s\n' '{"type":"register_tool","name":"normal","description":"normal","schema":{"type":"object"}}' | |
| 28 | + | printf '%s\n' '{"type":"ready"}' | |
| 29 | + | while IFS= read -r line; do | |
| 30 | + | case "$line" in | |
| 31 | + | *'"type":"shutdown"'*) printf '%s\n' '{"type":"shutdown_ack"}'; exit 0;; | |
| 32 | + | esac | |
| 33 | + | done | |
| 34 | + | ` | |
| 35 | + | if err := os.WriteFile(filepath.Join(extDir, "run.sh"), []byte(script), 0o755); err != nil { | |
| 36 | + | t.Fatal(err) | |
| 37 | + | } | |
| 38 | + | manifest, _ := json.Marshal(map[string]any{"name": "adapter-mock", "exec": "./run.sh"}) | |
| 39 | + | if err := os.WriteFile(filepath.Join(extDir, "extension.json"), manifest, 0o644); err != nil { | |
| 40 | + | t.Fatal(err) | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | mgr := extensions.New(root, "", "0.0.0", "openai", "test", nonInteractiveExtHooks{}) | |
| 44 | + | if errs := mgr.Discover(context.Background()); len(errs) != 0 { | |
| 45 | + | t.Fatalf("discover errs: %v", errs) | |
| 46 | + | } | |
| 47 | + | defer mgr.Stop(2 * time.Second) | |
| 48 | + | mgr.WaitForReady(time.Second) | |
| 49 | + | ||
| 50 | + | adapter := &extToolAdapter{mgr: mgr} | |
| 51 | + | infos := adapter.Tools() | |
| 52 | + | if len(infos) != 2 { | |
| 53 | + | t.Fatalf("got %d tools, want 2: %#v", len(infos), infos) | |
| 54 | + | } | |
| 55 | + | for _, info := range infos { | |
| 56 | + | _ = adapter.NewExtensionTool(info) | |
| 57 | + | wantInteractive := info.Name == "ask" | |
| 58 | + | if info.Interactive != wantInteractive { | |
| 59 | + | t.Fatalf("tool %q interactive metadata = %v, want %v", info.Name, info.Interactive, wantInteractive) | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | } | |
| @@ -0,0 +1,62 @@ | |||||
| 1 | + | package agent | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "encoding/json" | |||
| 6 | + | "os" | |||
| 7 | + | "path/filepath" | |||
| 8 | + | "runtime" | |||
| 9 | + | "testing" | |||
| 10 | + | "time" | |||
| 11 | + | ||||
| 12 | + | "github.com/patriceckhart/zot/packages/agent/extensions" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | func TestExtToolAdapterPreservesInteractiveTimeoutPolicy(t *testing.T) { | |||
| 16 | + | if runtime.GOOS == "windows" { | |||
| 17 | + | t.Skip("mock uses /bin/sh; skip on windows") | |||
| 18 | + | } | |||
| 19 | + | root := t.TempDir() | |||
| 20 | + | extDir := filepath.Join(root, "extensions", "adapter-mock") | |||
| 21 | + | if err := os.MkdirAll(extDir, 0o755); err != nil { | |||
| 22 | + | t.Fatal(err) | |||
| 23 | + | } | |||
| 24 | + | script := `#!/bin/sh | |||
| 25 | + | printf '%s\n' '{"type":"hello","name":"adapter-mock","version":"0.1","capabilities":["tools"]}' | |||
| 26 | + | printf '%s\n' '{"type":"register_tool","name":"ask","description":"interactive","schema":{"type":"object"},"interactive":true}' | |||
| 27 | + | printf '%s\n' '{"type":"register_tool","name":"normal","description":"normal","schema":{"type":"object"}}' | |||
| 28 | + | printf '%s\n' '{"type":"ready"}' | |||
| 29 | + | while IFS= read -r line; do | |||
| 30 | + | case "$line" in | |||
| 31 | + | *'"type":"shutdown"'*) printf '%s\n' '{"type":"shutdown_ack"}'; exit 0;; | |||
| 32 | + | esac | |||
| 33 | + | done | |||
| 34 | + | ` | |||
| 35 | + | if err := os.WriteFile(filepath.Join(extDir, "run.sh"), []byte(script), 0o755); err != nil { | |||
| 36 | + | t.Fatal(err) | |||
| 37 | + | } | |||
| 38 | + | manifest, _ := json.Marshal(map[string]any{"name": "adapter-mock", "exec": "./run.sh"}) | |||
| 39 | + | if err := os.WriteFile(filepath.Join(extDir, "extension.json"), manifest, 0o644); err != nil { | |||
| 40 | + | t.Fatal(err) | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | mgr := extensions.New(root, "", "0.0.0", "openai", "test", nonInteractiveExtHooks{}) | |||
| 44 | + | if errs := mgr.Discover(context.Background()); len(errs) != 0 { | |||
| 45 | + | t.Fatalf("discover errs: %v", errs) | |||
| 46 | + | } | |||
| 47 | + | defer mgr.Stop(2 * time.Second) | |||
| 48 | + | mgr.WaitForReady(time.Second) | |||
| 49 | + | ||||
| 50 | + | adapter := &extToolAdapter{mgr: mgr} | |||
| 51 | + | infos := adapter.Tools() | |||
| 52 | + | if len(infos) != 2 { | |||
| 53 | + | t.Fatalf("got %d tools, want 2: %#v", len(infos), infos) | |||
| 54 | + | } | |||
| 55 | + | for _, info := range infos { | |||
| 56 | + | _ = adapter.NewExtensionTool(info) | |||
| 57 | + | wantInteractive := info.Name == "ask" | |||
| 58 | + | if info.Interactive != wantInteractive { | |||
| 59 | + | t.Fatalf("tool %q interactive metadata = %v, want %v", info.Name, info.Interactive, wantInteractive) | |||
| 60 | + | } | |||
| 61 | + | } | |||
| 62 | + | } | |||