From ded7c81968be0f46bfb9026b6ed95fe9d49039bc Mon Sep 17 00:00:00 2001 From: Dan Bader Date: Sun, 24 May 2026 15:08:40 +0200 Subject: [PATCH] agent: resolve runtime assets from binary directory --- README.md | 8 +++- ds4_agent.c | 127 ++++++++++++++++++++++++++++++++++++++++++++++++++++ ds4_metal.m | 6 +++ 3 files changed, 139 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1c380328..79c94851 100644 --- a/README.md +++ b/README.md @@ -216,8 +216,12 @@ future saves and is derived from the first user prompt and creation time. conversation text and title but removes the heavy KV payload; switching to a stripped session rebuilds the KV cache by prefilling the saved text. -Use `--chdir /path/to/ds4` when launching `ds4-agent` from another directory, -so relative runtime files such as `metal/*.metal` resolve from the project tree. +When launched from another project directory, `ds4-agent` keeps the caller's +current directory as its workspace while resolving default runtime files such +as `ds4flash.gguf` and `metal/*.metal` from the directory containing the +`ds4-agent` binary. Use `--runtime-root /path/to/ds4` only when those assets +live somewhere else. `--chdir` is still available when you intentionally want +to change the agent process cwd. However while the system already works, there is a lot of work to do in order to make it ready for prime time. When finally the agent will reach diff --git a/ds4_agent.c b/ds4_agent.c index 41f19fac..8b937c6d 100644 --- a/ds4_agent.c +++ b/ds4_agent.c @@ -26,6 +26,10 @@ #include #include +#ifdef __APPLE__ +#include +#endif + /* This is intentionally not in linenoise.h, but it is part of the existing * multiplexed editor implementation. The agent uses it only to restore text * after Enter is pressed while the model is still busy. */ @@ -61,6 +65,7 @@ typedef struct { ds4_engine_options engine; agent_generation_options gen; const char *chdir_path; + const char *runtime_root; bool non_interactive; } agent_config; @@ -329,6 +334,121 @@ static char *xstrndup(const char *s, size_t n) { return p; } +static bool agent_path_is_absolute(const char *path) { + return path && path[0] == '/'; +} + +static char *agent_path_join(const char *root, const char *rel) { + if (!root || !root[0]) return xstrdup(rel ? rel : ""); + if (!rel || !rel[0]) return xstrdup(root); + if (agent_path_is_absolute(rel)) return xstrdup(rel); + + size_t root_len = strlen(root); + while (root_len > 1 && root[root_len - 1] == '/') root_len--; + + size_t rel_len = strlen(rel); + char *out = xmalloc(root_len + 1 + rel_len + 1); + memcpy(out, root, root_len); + out[root_len] = '/'; + memcpy(out + root_len + 1, rel, rel_len + 1); + return out; +} + +static char *agent_absolute_path(const char *path, const char *opt) { + if (!path || !path[0]) { + fprintf(stderr, "ds4-agent: %s must not be empty\n", opt); + exit(2); + } + if (agent_path_is_absolute(path)) return xstrdup(path); + + char cwd[PATH_MAX]; + if (!getcwd(cwd, sizeof(cwd))) { + fprintf(stderr, "ds4-agent: failed to resolve %s: %s\n", + opt, strerror(errno)); + exit(2); + } + return agent_path_join(cwd, path); +} + +static char *agent_dirname_owned(char *path) { + char *slash = strrchr(path, '/'); + if (!slash) { + free(path); + return xstrdup("."); + } + if (slash == path) { + slash[1] = '\0'; + } else { + *slash = '\0'; + } + return path; +} + +static char *agent_realpath_or_absolute(const char *path) { + char resolved[PATH_MAX]; + if (realpath(path, resolved)) return xstrdup(resolved); + return agent_absolute_path(path, "executable path"); +} + +static char *agent_executable_dir(const char *argv0) { +#ifdef __APPLE__ + char stack_path[PATH_MAX]; + uint32_t len = sizeof(stack_path); + if (_NSGetExecutablePath(stack_path, &len) == 0) + return agent_dirname_owned(agent_realpath_or_absolute(stack_path)); + if (len > 0) { + char *buf = xmalloc((size_t)len + 1); + if (_NSGetExecutablePath(buf, &len) == 0) { + buf[len] = '\0'; + char *path = agent_realpath_or_absolute(buf); + free(buf); + return agent_dirname_owned(path); + } + free(buf); + } +#elif defined(__linux__) + char proc_path[PATH_MAX]; + ssize_t n = readlink("/proc/self/exe", proc_path, sizeof(proc_path) - 1); + if (n > 0) { + proc_path[n] = '\0'; + return agent_dirname_owned(agent_realpath_or_absolute(proc_path)); + } +#endif + + if (argv0 && strchr(argv0, '/')) + return agent_dirname_owned(agent_realpath_or_absolute(argv0)); + return NULL; +} + +static void agent_apply_runtime_root(agent_config *c, bool model_path_set, + const char *argv0) { + char *detected_root = NULL; + const char *env_root = getenv("DS4_RUNTIME_ROOT"); + if ((!c->runtime_root || !c->runtime_root[0]) && env_root && env_root[0]) + c->runtime_root = env_root; + if (!c->runtime_root || !c->runtime_root[0]) { + detected_root = agent_executable_dir(argv0); + c->runtime_root = detected_root; + } + if (!c->runtime_root || !c->runtime_root[0]) return; + + c->runtime_root = agent_absolute_path(c->runtime_root, "--runtime-root"); + free(detected_root); + if (setenv("DS4_RUNTIME_ROOT", c->runtime_root, 1) != 0) { + fprintf(stderr, "ds4-agent: failed to set DS4_RUNTIME_ROOT: %s\n", + strerror(errno)); + exit(2); + } + + if (!model_path_set && + c->engine.model_path && c->engine.model_path[0] && + !agent_path_is_absolute(c->engine.model_path)) + { + c->engine.model_path = agent_path_join(c->runtime_root, + c->engine.model_path); + } +} + static void *xrealloc(void *ptr, size_t n) { void *p = realloc(ptr, n ? n : 1); if (!p) { @@ -494,6 +614,7 @@ static void usage(FILE *fp) { " --metal, --cuda, --cpu Select backend explicitly.\n" " -t, --threads N CPU helper threads.\n" " --chdir DIR Change working directory before loading runtime assets.\n" + " --runtime-root DIR Override runtime asset root. Default: ds4-agent's directory.\n" " --quality Prefer exact kernels where available.\n" " --warm-weights Touch mapped tensor pages before generation.\n" " --power N Target GPU duty cycle percentage, 1..100. Default: 100\n" @@ -543,6 +664,7 @@ static agent_config parse_options(int argc, char **argv) { }; bool steering_scale_set = false; + bool model_path_set = false; for (int i = 1; i < argc; i++) { const char *arg = argv[i]; if (!strcmp(arg, "-h") || !strcmp(arg, "--help")) { @@ -558,6 +680,7 @@ static agent_config parse_options(int argc, char **argv) { c.gen.trace_path = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "-m") || !strcmp(arg, "--model")) { c.engine.model_path = need_arg(&i, argc, argv, arg); + model_path_set = true; } else if (!strcmp(arg, "--mtp")) { c.engine.mtp_path = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "--mtp-draft")) { @@ -594,6 +717,8 @@ static agent_config parse_options(int argc, char **argv) { c.engine.n_threads = parse_int(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "--chdir")) { c.chdir_path = need_arg(&i, argc, argv, arg); + } else if (!strcmp(arg, "--runtime-root")) { + c.runtime_root = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "--quality")) { c.engine.quality = true; } else if (!strcmp(arg, "--power")) { @@ -621,6 +746,8 @@ static agent_config parse_options(int argc, char **argv) { if (c.engine.directional_steering_file && !steering_scale_set) c.engine.directional_steering_ffn = 1.0f; + + agent_apply_runtime_root(&c, model_path_set, argv[0]); return c; } diff --git a/ds4_metal.m b/ds4_metal.m index 65da60c6..96ce5ff9 100644 --- a/ds4_metal.m +++ b/ds4_metal.m @@ -1450,6 +1450,9 @@ void ds4_gpu_set_quality(bool quality) { static NSString *ds4_gpu_full_source(void) { NSString *base = [NSString stringWithUTF8String:ds4_gpu_source]; NSFileManager *fm = [NSFileManager defaultManager]; + const char *runtime_root_c = getenv("DS4_RUNTIME_ROOT"); + NSString *runtime_root = runtime_root_c && runtime_root_c[0] ? + [NSString stringWithUTF8String:runtime_root_c] : nil; /* * Kernels are kept as separate files for review, then concatenated into one * Metal library. Environment overrides are still honored so a diagnostic @@ -1484,6 +1487,9 @@ void ds4_gpu_set_quality(bool quality) { if (override_path && override_path[0]) { [paths addObject:[NSString stringWithUTF8String:override_path]]; } + if (runtime_root) { + [paths addObject:[runtime_root stringByAppendingPathComponent:spec[1]]]; + } [paths addObject:spec[1]]; [paths addObject:[@"./" stringByAppendingString:spec[1]]];